CRUD program: create / update / delete (#1226)
* Add scaffolding * Add create / update / delete instructions and tests * Update SerializationError -> IOError and program id * Address review feedback * Make initialize work with `create_with_seed` * Cargo fmt * Use offset for writing * Update crud -> record * More feedback * Remove rent * Update program id * Use official Solana crates 1.5.10 * Update Cargo lock and toml * Cargo fmt * Update record program version to 1.5.11 * Bump compute budget
This commit is contained in:
parent
87a849407f
commit
8fd6f8ec55
|
@ -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"
|
||||
|
|
|
@ -10,6 +10,7 @@ members = [
|
|||
"feature-proposal/cli",
|
||||
"libraries/math",
|
||||
"memo/program",
|
||||
"record/program",
|
||||
"shared-memory/program",
|
||||
"stake-pool/cli",
|
||||
"stake-pool/program",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
[package]
|
||||
name = "spl-record"
|
||||
version = "0.1.0"
|
||||
description = "Solana Program Library Record Program"
|
||||
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
|
||||
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"]
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1 @@
|
|||
ReciQBw6sQKH9TVVJQDnbnJ5W7FP539tPHjZhRF4E9r
|
|
@ -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)
|
||||
}
|
|
@ -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<RecordError> for ProgramError {
|
||||
fn from(e: RecordError) -> Self {
|
||||
ProgramError::Custom(e as u32)
|
||||
}
|
||||
}
|
||||
impl<T> DecodeError<T> for RecordError {
|
||||
fn type_of() -> &'static str {
|
||||
"Record Error"
|
||||
}
|
||||
}
|
|
@ -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<u8>,
|
||||
},
|
||||
|
||||
/// 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<u8>) -> 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(_)));
|
||||
}
|
||||
}
|
|
@ -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");
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(_)));
|
||||
}
|
||||
}
|
|
@ -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::<RecordData>())),
|
||||
get_packed_len::<RecordData>() 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::<RecordData>(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::<RecordData>())),
|
||||
get_packed_len::<RecordData>() 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::<RecordData>(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::<RecordData>(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::<RecordData>()))
|
||||
);
|
||||
}
|
||||
|
||||
#[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::<RecordData>(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::<RecordData>(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)
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue