diff --git a/Cargo.lock b/Cargo.lock index 147a5298..18d8e2cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3796,6 +3796,21 @@ dependencies = [ "solana-program", ] +[[package]] +name = "spl-record" +version = "0.1.0" +dependencies = [ + "borsh 0.8.1", + "borsh-derive 0.8.1", + "num-derive", + "num-traits", + "solana-program", + "solana-program-test", + "solana-sdk", + "thiserror", + "tokio 0.3.6", +] + [[package]] name = "spl-shared-memory" version = "2.0.6" diff --git a/Cargo.toml b/Cargo.toml index 3425391d..2666b639 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "feature-proposal/cli", "libraries/math", "memo/program", + "record/program", "shared-memory/program", "stake-pool/cli", "stake-pool/program", diff --git a/record/program/Cargo.toml b/record/program/Cargo.toml new file mode 100644 index 00000000..111e93d8 --- /dev/null +++ b/record/program/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "spl-record" +version = "0.1.0" +description = "Solana Program Library Record Program" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2018" + +[features] +no-entrypoint = [] +test-bpf = [] + +[dependencies] +borsh = "0.8.1" +borsh-derive = "0.8.1" +num-derive = "0.3" +num-traits = "0.2" +solana-program = "1.5.11" +thiserror = "1.0" + +[dev-dependencies] +solana-program-test = "1.5.11" +solana-sdk = "1.5.11" +tokio = { version = "0.3", features = ["macros"]} + +[lib] +crate-type = ["cdylib", "lib"] + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/record/program/Xargo.toml b/record/program/Xargo.toml new file mode 100644 index 00000000..1744f098 --- /dev/null +++ b/record/program/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/record/program/program-id.md b/record/program/program-id.md new file mode 100644 index 00000000..09f28307 --- /dev/null +++ b/record/program/program-id.md @@ -0,0 +1 @@ +ReciQBw6sQKH9TVVJQDnbnJ5W7FP539tPHjZhRF4E9r diff --git a/record/program/src/entrypoint.rs b/record/program/src/entrypoint.rs new file mode 100644 index 00000000..7cf225a1 --- /dev/null +++ b/record/program/src/entrypoint.rs @@ -0,0 +1,16 @@ +//! Program entrypoint + +#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))] + +use solana_program::{ + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey, +}; + +entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + crate::processor::process_instruction(program_id, accounts, instruction_data) +} diff --git a/record/program/src/error.rs b/record/program/src/error.rs new file mode 100644 index 00000000..53763915 --- /dev/null +++ b/record/program/src/error.rs @@ -0,0 +1,27 @@ +//! Error types + +use num_derive::FromPrimitive; +use solana_program::{decode_error::DecodeError, program_error::ProgramError}; +use thiserror::Error; + +/// Errors that may be returned by the program. +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum RecordError { + /// Incorrect authority provided on update or delete + #[error("Incorrect authority provided on update or delete")] + IncorrectAuthority, + + /// Calculation overflow + #[error("Calculation overflow")] + Overflow, +} +impl From for ProgramError { + fn from(e: RecordError) -> Self { + ProgramError::Custom(e as u32) + } +} +impl DecodeError for RecordError { + fn type_of() -> &'static str { + "Record Error" + } +} diff --git a/record/program/src/instruction.rs b/record/program/src/instruction.rs new file mode 100644 index 00000000..bd86bd71 --- /dev/null +++ b/record/program/src/instruction.rs @@ -0,0 +1,173 @@ +//! Program instructions + +use crate::id; +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, +}; + +/// Instructions supported by the program +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, PartialEq)] +pub enum RecordInstruction { + /// Create a new record + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be uninitialized + /// 1. `[]` Record authority + Initialize, + + /// Write to the provided record account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be previously initialized + /// 1. `[signer]` Current record authority + Write { + /// Offset to start writing record, expressed as `u64`. + offset: u64, + /// Data to replace the existing record data + data: Vec, + }, + + /// Update the authority of the provided record account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be previously initialized + /// 1. `[signer]` Current record authority + /// 2. `[]` New record authority + SetAuthority, + + /// Close the provided record account, draining lamports to recipient account + /// + /// Accounts expected by this instruction: + /// + /// 0. `[writable]` Record account, must be previously initialized + /// 1. `[signer]` Record authority + /// 2. `[]` Receiver of account lamports + CloseAccount, +} + +/// Create a `RecordInstruction::Initialize` instruction +pub fn initialize(record_account: &Pubkey, authority: &Pubkey) -> Instruction { + Instruction::new_with_borsh( + id(), + &RecordInstruction::Initialize, + vec![ + AccountMeta::new(*record_account, false), + AccountMeta::new_readonly(*authority, false), + ], + ) +} + +/// Create a `RecordInstruction::Write` instruction +pub fn write(record_account: &Pubkey, signer: &Pubkey, offset: u64, data: Vec) -> Instruction { + Instruction::new_with_borsh( + id(), + &RecordInstruction::Write { offset, data }, + vec![ + AccountMeta::new(*record_account, false), + AccountMeta::new_readonly(*signer, true), + ], + ) +} + +/// Create a `RecordInstruction::SetAuthority` instruction +pub fn set_authority( + record_account: &Pubkey, + signer: &Pubkey, + new_authority: &Pubkey, +) -> Instruction { + Instruction::new_with_borsh( + id(), + &RecordInstruction::SetAuthority, + vec![ + AccountMeta::new(*record_account, false), + AccountMeta::new_readonly(*signer, true), + AccountMeta::new_readonly(*new_authority, false), + ], + ) +} + +/// Create a `RecordInstruction::CloseAccount` instruction +pub fn close_account(record_account: &Pubkey, signer: &Pubkey, receiver: &Pubkey) -> Instruction { + Instruction::new_with_borsh( + id(), + &RecordInstruction::CloseAccount, + vec![ + AccountMeta::new(*record_account, false), + AccountMeta::new_readonly(*signer, true), + AccountMeta::new(*receiver, false), + ], + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::state::tests::TEST_DATA; + use solana_program::program_error::ProgramError; + + #[test] + fn serialize_initialize() { + let instruction = RecordInstruction::Initialize; + let expected = vec![0]; + assert_eq!(instruction.try_to_vec().unwrap(), expected); + assert_eq!( + RecordInstruction::try_from_slice(&expected).unwrap(), + instruction + ); + } + + #[test] + fn serialize_write() { + let data = TEST_DATA.try_to_vec().unwrap(); + let offset = 0u64; + let instruction = RecordInstruction::Write { + offset: 0, + data: data.clone(), + }; + let mut expected = vec![1]; + expected.extend_from_slice(&offset.to_le_bytes()); + expected.append(&mut data.try_to_vec().unwrap()); + assert_eq!(instruction.try_to_vec().unwrap(), expected); + assert_eq!( + RecordInstruction::try_from_slice(&expected).unwrap(), + instruction + ); + } + + #[test] + fn serialize_set_authority() { + let instruction = RecordInstruction::SetAuthority; + let expected = vec![2]; + assert_eq!(instruction.try_to_vec().unwrap(), expected); + assert_eq!( + RecordInstruction::try_from_slice(&expected).unwrap(), + instruction + ); + } + + #[test] + fn serialize_close_account() { + let instruction = RecordInstruction::CloseAccount; + let expected = vec![3]; + assert_eq!(instruction.try_to_vec().unwrap(), expected); + assert_eq!( + RecordInstruction::try_from_slice(&expected).unwrap(), + instruction + ); + } + + #[test] + fn deserialize_invalid_instruction() { + let mut expected = vec![12]; + expected.append(&mut TEST_DATA.try_to_vec().unwrap()); + let err: ProgramError = RecordInstruction::try_from_slice(&expected) + .unwrap_err() + .into(); + assert!(matches!(err, ProgramError::BorshIoError(_))); + } +} diff --git a/record/program/src/lib.rs b/record/program/src/lib.rs new file mode 100644 index 00000000..94db6650 --- /dev/null +++ b/record/program/src/lib.rs @@ -0,0 +1,13 @@ +//! Record program +#![deny(missing_docs)] + +mod entrypoint; +pub mod error; +pub mod instruction; +pub mod processor; +pub mod state; + +// Export current SDK types for downstream users building with a different SDK version +pub use solana_program; + +solana_program::declare_id!("ReciQBw6sQKH9TVVJQDnbnJ5W7FP539tPHjZhRF4E9r"); diff --git a/record/program/src/processor.rs b/record/program/src/processor.rs new file mode 100644 index 00000000..9985dd89 --- /dev/null +++ b/record/program/src/processor.rs @@ -0,0 +1,121 @@ +//! Program state processor + +use { + crate::{ + error::RecordError, + instruction::RecordInstruction, + state::{Data, RecordData}, + }, + borsh::{BorshDeserialize, BorshSerialize}, + solana_program::{ + account_info::{next_account_info, AccountInfo}, + entrypoint::ProgramResult, + msg, + program_error::ProgramError, + program_pack::IsInitialized, + pubkey::Pubkey, + }, +}; + +fn check_authority(authority_info: &AccountInfo, expected_authority: &Pubkey) -> ProgramResult { + if expected_authority != authority_info.key { + msg!("Incorrect record authority provided"); + return Err(RecordError::IncorrectAuthority.into()); + } + if !authority_info.is_signer { + msg!("Record authority signature missing"); + return Err(ProgramError::MissingRequiredSignature); + } + Ok(()) +} + +/// Instruction processor +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + input: &[u8], +) -> ProgramResult { + let instruction = RecordInstruction::try_from_slice(input)?; + let account_info_iter = &mut accounts.iter(); + + match instruction { + RecordInstruction::Initialize => { + msg!("RecordInstruction::Initialize"); + + let data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + + let mut account_data = RecordData::try_from_slice(*data_info.data.borrow())?; + if account_data.is_initialized() { + msg!("Record account already initialized"); + return Err(ProgramError::AccountAlreadyInitialized); + } + + account_data.authority = *authority_info.key; + account_data.version = RecordData::CURRENT_VERSION; + account_data + .serialize(&mut *data_info.data.borrow_mut()) + .map_err(|e| e.into()) + } + + RecordInstruction::Write { offset, data } => { + msg!("RecordInstruction::Write"); + let data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let account_data = RecordData::try_from_slice(&data_info.data.borrow())?; + if !account_data.is_initialized() { + msg!("Record account not initialized"); + return Err(ProgramError::UninitializedAccount); + } + check_authority(authority_info, &account_data.authority)?; + let start = RecordData::WRITABLE_START_INDEX + offset as usize; + let end = start + data.len(); + if end > data_info.data.borrow().len() { + Err(ProgramError::AccountDataTooSmall) + } else { + data_info.data.borrow_mut()[start..end].copy_from_slice(&data); + Ok(()) + } + } + + RecordInstruction::SetAuthority => { + msg!("RecordInstruction::SetAuthority"); + let data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let new_authority_info = next_account_info(account_info_iter)?; + let mut account_data = RecordData::try_from_slice(&data_info.data.borrow())?; + if !account_data.is_initialized() { + msg!("Record account not initialized"); + return Err(ProgramError::UninitializedAccount); + } + check_authority(authority_info, &account_data.authority)?; + account_data.authority = *new_authority_info.key; + account_data + .serialize(&mut *data_info.data.borrow_mut()) + .map_err(|e| e.into()) + } + + RecordInstruction::CloseAccount => { + msg!("RecordInstruction::CloseAccount"); + let data_info = next_account_info(account_info_iter)?; + let authority_info = next_account_info(account_info_iter)?; + let destination_info = next_account_info(account_info_iter)?; + let mut account_data = RecordData::try_from_slice(&data_info.data.borrow())?; + if !account_data.is_initialized() { + msg!("Record not initialized"); + return Err(ProgramError::UninitializedAccount); + } + check_authority(authority_info, &account_data.authority)?; + let destination_starting_lamports = destination_info.lamports(); + let data_lamports = data_info.lamports(); + **data_info.lamports.borrow_mut() = 0; + **destination_info.lamports.borrow_mut() = destination_starting_lamports + .checked_add(data_lamports) + .ok_or(RecordError::Overflow)?; + account_data.data = Data::default(); + account_data + .serialize(&mut *data_info.data.borrow_mut()) + .map_err(|e| e.into()) + } + } +} diff --git a/record/program/src/state.rs b/record/program/src/state.rs new file mode 100644 index 00000000..0bdc5475 --- /dev/null +++ b/record/program/src/state.rs @@ -0,0 +1,90 @@ +//! Program state +use { + borsh::{BorshDeserialize, BorshSchema, BorshSerialize}, + solana_program::{program_pack::IsInitialized, pubkey::Pubkey}, +}; + +/// Struct wrapping data and providing metadata +#[derive(Clone, Debug, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq)] +pub struct RecordData { + /// Struct version, allows for upgrades to the program + pub version: u8, + + /// The account allowed to update the data + pub authority: Pubkey, + + /// The data contained by the account, could be anything serializable + pub data: Data, +} + +/// Struct just for data +#[derive(Clone, Debug, Default, BorshSerialize, BorshDeserialize, BorshSchema, PartialEq)] +pub struct Data { + /// The data contained by the account, could be anything or serializable + pub bytes: [u8; Self::DATA_SIZE], +} + +impl Data { + /// very small data for easy testing + pub const DATA_SIZE: usize = 8; +} + +impl RecordData { + /// Version to fill in on new created accounts + pub const CURRENT_VERSION: u8 = 1; + + /// Start of writable account data, after version and authority + pub const WRITABLE_START_INDEX: usize = 33; +} + +impl IsInitialized for RecordData { + /// Is initialized + fn is_initialized(&self) -> bool { + self.version == Self::CURRENT_VERSION + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use solana_program::program_error::ProgramError; + + /// Version for tests + pub const TEST_VERSION: u8 = 1; + /// Pubkey for tests + pub const TEST_PUBKEY: Pubkey = Pubkey::new_from_array([100; 32]); + /// Bytes for tests + pub const TEST_BYTES: [u8; Data::DATA_SIZE] = [42; Data::DATA_SIZE]; + /// Data for tests + pub const TEST_DATA: Data = Data { bytes: TEST_BYTES }; + /// RecordData for tests + pub const TEST_RECORD_DATA: RecordData = RecordData { + version: TEST_VERSION, + authority: TEST_PUBKEY, + data: TEST_DATA, + }; + + #[test] + fn serialize_data() { + let mut expected = vec![]; + expected.push(TEST_VERSION); + expected.extend_from_slice(&TEST_PUBKEY.to_bytes()); + expected.extend_from_slice(&TEST_DATA.bytes); + assert_eq!(TEST_RECORD_DATA.try_to_vec().unwrap(), expected); + assert_eq!( + RecordData::try_from_slice(&expected).unwrap(), + TEST_RECORD_DATA + ); + } + + #[test] + fn deserialize_invalid_slice() { + let data = [200; Data::DATA_SIZE - 1]; + let mut expected = vec![]; + expected.push(TEST_VERSION); + expected.extend_from_slice(&TEST_PUBKEY.to_bytes()); + expected.extend_from_slice(&data); + let err: ProgramError = RecordData::try_from_slice(&expected).unwrap_err().into(); + assert!(matches!(err, ProgramError::BorshIoError(_))); + } +} diff --git a/record/program/tests/functional.rs b/record/program/tests/functional.rs new file mode 100644 index 00000000..8a2d8e91 --- /dev/null +++ b/record/program/tests/functional.rs @@ -0,0 +1,552 @@ +// Mark this test as BPF-only due to current `ProgramTest` limitations when CPIing into the system program +#![cfg(feature = "test-bpf")] + +use { + borsh::BorshSerialize, + solana_program::{ + borsh::get_packed_len, + instruction::{AccountMeta, Instruction, InstructionError}, + pubkey::Pubkey, + rent::Rent, + system_instruction, + }, + solana_program_test::{processor, ProgramTest, ProgramTestContext}, + solana_sdk::{ + signature::{Keypair, Signer}, + transaction::{Transaction, TransactionError}, + transport, + }, + spl_record::{ + error::RecordError, + id, instruction, + processor::process_instruction, + state::{Data, RecordData}, + }, +}; + +fn program_test() -> ProgramTest { + ProgramTest::new("spl_record", id(), processor!(process_instruction)) +} + +async fn initialize_storage_account( + context: &mut ProgramTestContext, + authority: &Keypair, + account: &Keypair, + data: Data, +) -> transport::Result<()> { + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account( + &context.payer.pubkey(), + &account.pubkey(), + 1.max(Rent::default().minimum_balance(get_packed_len::())), + get_packed_len::() as u64, + &id(), + ), + instruction::initialize(&account.pubkey(), &authority.pubkey()), + instruction::write( + &account.pubkey(), + &authority.pubkey(), + 0, + data.try_to_vec().unwrap(), + ), + ], + Some(&context.payer.pubkey()), + &[&context.payer, account, authority], + context.last_blockhash, + ); + context.banks_client.process_transaction(transaction).await +} + +#[tokio::test] +async fn initialize_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [111u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data.clone()) + .await + .unwrap(); + let account_data = context + .banks_client + .get_account_data_with_borsh::(account.pubkey()) + .await + .unwrap(); + assert_eq!(account_data.data, data); + assert_eq!(account_data.authority, authority.pubkey()); + assert_eq!(account_data.version, RecordData::CURRENT_VERSION); +} + +#[tokio::test] +async fn initialize_with_seed_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let seed = "storage"; + let account = Pubkey::create_with_seed(&authority.pubkey(), seed, &id()).unwrap(); + let data = Data { + bytes: [111u8; Data::DATA_SIZE], + }; + let transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::create_account_with_seed( + &context.payer.pubkey(), + &account, + &authority.pubkey(), + seed, + 1.max(Rent::default().minimum_balance(get_packed_len::())), + get_packed_len::() as u64, + &id(), + ), + instruction::initialize(&account, &authority.pubkey()), + instruction::write(&account, &authority.pubkey(), 0, data.try_to_vec().unwrap()), + ], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + let account_data = context + .banks_client + .get_account_data_with_borsh::(account) + .await + .unwrap(); + assert_eq!(account_data.data, data); + assert_eq!(account_data.authority, authority.pubkey()); + assert_eq!(account_data.version, RecordData::CURRENT_VERSION); +} + +#[tokio::test] +async fn initialize_twice_fail() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [111u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data) + .await + .unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[instruction::initialize( + &account.pubkey(), + &authority.pubkey(), + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::AccountAlreadyInitialized) + ); +} + +#[tokio::test] +async fn write_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [222u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data) + .await + .unwrap(); + + let new_data = Data { + bytes: [200u8; Data::DATA_SIZE], + }; + let transaction = Transaction::new_signed_with_payer( + &[instruction::write( + &account.pubkey(), + &authority.pubkey(), + 0, + new_data.try_to_vec().unwrap(), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account_data = context + .banks_client + .get_account_data_with_borsh::(account.pubkey()) + .await + .unwrap(); + assert_eq!(account_data.data, new_data); + assert_eq!(account_data.authority, authority.pubkey()); + assert_eq!(account_data.version, RecordData::CURRENT_VERSION); +} + +#[tokio::test] +async fn write_fail_wrong_authority() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [222u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data) + .await + .unwrap(); + + let new_data = Data { + bytes: [200u8; Data::DATA_SIZE], + }; + let wrong_authority = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &[instruction::write( + &account.pubkey(), + &wrong_authority.pubkey(), + 0, + new_data.try_to_vec().unwrap(), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(RecordError::IncorrectAuthority as u32) + ) + ); +} + +#[tokio::test] +async fn write_fail_unsigned() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [222u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data) + .await + .unwrap(); + + let data = Data { + bytes: [200u8; Data::DATA_SIZE], + } + .try_to_vec() + .unwrap(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_borsh( + id(), + &instruction::RecordInstruction::Write { offset: 0, data }, + vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(authority.pubkey(), false), + ], + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); +} + +#[tokio::test] +async fn close_account_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [222u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data) + .await + .unwrap(); + let recipient = Pubkey::new_unique(); + + let transaction = Transaction::new_signed_with_payer( + &[instruction::close_account( + &account.pubkey(), + &authority.pubkey(), + &recipient, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account = context + .banks_client + .get_account(recipient) + .await + .unwrap() + .unwrap(); + assert_eq!( + account.lamports, + 1.max(Rent::default().minimum_balance(get_packed_len::())) + ); +} + +#[tokio::test] +async fn close_account_fail_wrong_authority() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [222u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data) + .await + .unwrap(); + + let wrong_authority = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_borsh( + id(), + &instruction::RecordInstruction::CloseAccount, + vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(wrong_authority.pubkey(), true), + AccountMeta::new(Pubkey::new_unique(), false), + ], + )], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(RecordError::IncorrectAuthority as u32) + ) + ); +} + +#[tokio::test] +async fn close_account_fail_unsigned() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [222u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data) + .await + .unwrap(); + + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_borsh( + id(), + &instruction::RecordInstruction::CloseAccount, + vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(authority.pubkey(), false), + AccountMeta::new(Pubkey::new_unique(), false), + ], + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); +} + +#[tokio::test] +async fn set_authority_success() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [222u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data) + .await + .unwrap(); + let new_authority = Keypair::new(); + + let transaction = Transaction::new_signed_with_payer( + &[instruction::set_authority( + &account.pubkey(), + &authority.pubkey(), + &new_authority.pubkey(), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account_data = context + .banks_client + .get_account_data_with_borsh::(account.pubkey()) + .await + .unwrap(); + assert_eq!(account_data.authority, new_authority.pubkey()); + + let new_data = Data { + bytes: [200u8; Data::DATA_SIZE], + }; + let transaction = Transaction::new_signed_with_payer( + &[instruction::write( + &account.pubkey(), + &new_authority.pubkey(), + 0, + new_data.try_to_vec().unwrap(), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &new_authority], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let account_data = context + .banks_client + .get_account_data_with_borsh::(account.pubkey()) + .await + .unwrap(); + assert_eq!(account_data.data, new_data); + assert_eq!(account_data.authority, new_authority.pubkey()); + assert_eq!(account_data.version, RecordData::CURRENT_VERSION); +} + +#[tokio::test] +async fn set_authority_fail_wrong_authority() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [222u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data) + .await + .unwrap(); + + let wrong_authority = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_borsh( + id(), + &instruction::RecordInstruction::SetAuthority, + vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(wrong_authority.pubkey(), true), + AccountMeta::new(Pubkey::new_unique(), false), + ], + )], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_authority], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError( + 0, + InstructionError::Custom(RecordError::IncorrectAuthority as u32) + ) + ); +} + +#[tokio::test] +async fn set_authority_fail_unsigned() { + let mut context = program_test().start_with_context().await; + + let authority = Keypair::new(); + let account = Keypair::new(); + let data = Data { + bytes: [222u8; Data::DATA_SIZE], + }; + initialize_storage_account(&mut context, &authority, &account, data) + .await + .unwrap(); + + let transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_borsh( + id(), + &instruction::RecordInstruction::SetAuthority, + vec![ + AccountMeta::new(account.pubkey(), false), + AccountMeta::new_readonly(authority.pubkey(), false), + AccountMeta::new(Pubkey::new_unique(), false), + ], + )], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + assert_eq!( + context + .banks_client + .process_transaction(transaction) + .await + .unwrap_err() + .unwrap(), + TransactionError::InstructionError(0, InstructionError::MissingRequiredSignature) + ); +}