Get access to runtime errors in Budget unit-tests
This commit is contained in:
parent
60437a8dcb
commit
70b45de012
|
@ -1,11 +1,12 @@
|
|||
//! budget program
|
||||
use bincode::deserialize;
|
||||
use bincode::{deserialize, serialize};
|
||||
use chrono::prelude::{DateTime, Utc};
|
||||
use log::*;
|
||||
use solana_budget_api::budget_instruction::BudgetInstruction;
|
||||
use solana_budget_api::budget_state::{BudgetError, BudgetState};
|
||||
use solana_budget_api::payment_plan::Witness;
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::native_program::ProgramError;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
/// Process a Witness Signature. Any payment plans waiting on this signature
|
||||
|
@ -16,10 +17,7 @@ fn apply_signature(
|
|||
) -> Result<(), BudgetError> {
|
||||
let mut final_payment = None;
|
||||
if let Some(ref mut expr) = budget_state.pending_budget {
|
||||
let key = match keyed_accounts[0].signer_key() {
|
||||
None => return Err(BudgetError::UnsignedKey),
|
||||
Some(key) => key,
|
||||
};
|
||||
let key = keyed_accounts[0].signer_key().unwrap();
|
||||
expr.apply_witness(&Witness::Signature, key);
|
||||
final_payment = expr.final_payment();
|
||||
}
|
||||
|
@ -55,10 +53,7 @@ fn apply_timestamp(
|
|||
let mut final_payment = None;
|
||||
|
||||
if let Some(ref mut expr) = budget_state.pending_budget {
|
||||
let key = match keyed_accounts[0].signer_key() {
|
||||
None => return Err(BudgetError::UnsignedKey),
|
||||
Some(key) => key,
|
||||
};
|
||||
let key = keyed_accounts[0].signer_key().unwrap();
|
||||
expr.apply_witness(&Witness::Timestamp(dt), key);
|
||||
final_payment = expr.final_payment();
|
||||
}
|
||||
|
@ -75,83 +70,75 @@ fn apply_timestamp(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_debits(
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
instruction: &BudgetInstruction,
|
||||
) -> Result<(), BudgetError> {
|
||||
data: &[u8],
|
||||
) -> Result<(), ProgramError> {
|
||||
let instruction = deserialize(data).map_err(|err| {
|
||||
info!("Invalid transaction data: {:?} {:?}", data, err);
|
||||
ProgramError::InvalidInstructionData
|
||||
})?;
|
||||
|
||||
trace!("process_instruction: {:?}", instruction);
|
||||
|
||||
match instruction {
|
||||
BudgetInstruction::InitializeAccount(expr) => {
|
||||
let expr = expr.clone();
|
||||
if let Some(payment) = expr.final_payment() {
|
||||
keyed_accounts[1].account.lamports = 0;
|
||||
keyed_accounts[0].account.lamports += payment.lamports;
|
||||
Ok(())
|
||||
} else {
|
||||
let existing = BudgetState::deserialize(&keyed_accounts[0].account.data).ok();
|
||||
if Some(true) == existing.map(|x| x.initialized) {
|
||||
trace!("contract already exists");
|
||||
Err(BudgetError::ContractAlreadyExists)
|
||||
} else {
|
||||
let mut budget_state = BudgetState::default();
|
||||
budget_state.pending_budget = Some(expr);
|
||||
budget_state.initialized = true;
|
||||
budget_state.serialize(&mut keyed_accounts[0].account.data)
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let existing = BudgetState::deserialize(&keyed_accounts[0].account.data).ok();
|
||||
if Some(true) == existing.map(|x| x.initialized) {
|
||||
trace!("contract already exists");
|
||||
return Err(ProgramError::AccountAlreadyInitialized);
|
||||
}
|
||||
let mut budget_state = BudgetState::default();
|
||||
budget_state.pending_budget = Some(expr);
|
||||
budget_state.initialized = true;
|
||||
budget_state.serialize(&mut keyed_accounts[0].account.data)
|
||||
}
|
||||
BudgetInstruction::ApplyTimestamp(dt) => {
|
||||
if let Ok(mut budget_state) = BudgetState::deserialize(&keyed_accounts[1].account.data)
|
||||
{
|
||||
if !budget_state.is_pending() {
|
||||
Err(BudgetError::ContractNotPending)
|
||||
} else if !budget_state.initialized {
|
||||
trace!("contract is uninitialized");
|
||||
Err(BudgetError::UninitializedContract)
|
||||
} else {
|
||||
trace!("apply timestamp");
|
||||
apply_timestamp(&mut budget_state, keyed_accounts, *dt)?;
|
||||
trace!("apply timestamp committed");
|
||||
budget_state.serialize(&mut keyed_accounts[1].account.data)
|
||||
}
|
||||
} else {
|
||||
Err(BudgetError::UninitializedContract)
|
||||
let mut budget_state = BudgetState::deserialize(&keyed_accounts[1].account.data)?;
|
||||
if !budget_state.is_pending() {
|
||||
return Ok(()); // Nothing to do here.
|
||||
}
|
||||
if !budget_state.initialized {
|
||||
trace!("contract is uninitialized");
|
||||
return Err(ProgramError::UninitializedAccount);
|
||||
}
|
||||
if keyed_accounts[0].signer_key().is_none() {
|
||||
return Err(ProgramError::MissingRequiredSignature);
|
||||
}
|
||||
trace!("apply timestamp");
|
||||
apply_timestamp(&mut budget_state, keyed_accounts, dt)
|
||||
.map_err(|e| ProgramError::CustomError(serialize(&e).unwrap()))?;
|
||||
trace!("apply timestamp committed");
|
||||
budget_state.serialize(&mut keyed_accounts[1].account.data)
|
||||
}
|
||||
BudgetInstruction::ApplySignature => {
|
||||
if let Ok(mut budget_state) = BudgetState::deserialize(&keyed_accounts[1].account.data)
|
||||
{
|
||||
if !budget_state.is_pending() {
|
||||
Err(BudgetError::ContractNotPending)
|
||||
} else if !budget_state.initialized {
|
||||
trace!("contract is uninitialized");
|
||||
Err(BudgetError::UninitializedContract)
|
||||
} else {
|
||||
trace!("apply signature");
|
||||
apply_signature(&mut budget_state, keyed_accounts)?;
|
||||
trace!("apply signature committed");
|
||||
budget_state.serialize(&mut keyed_accounts[1].account.data)
|
||||
}
|
||||
} else {
|
||||
Err(BudgetError::UninitializedContract)
|
||||
let mut budget_state = BudgetState::deserialize(&keyed_accounts[1].account.data)?;
|
||||
if !budget_state.is_pending() {
|
||||
return Ok(()); // Nothing to do here.
|
||||
}
|
||||
if !budget_state.initialized {
|
||||
trace!("contract is uninitialized");
|
||||
return Err(ProgramError::UninitializedAccount);
|
||||
}
|
||||
if keyed_accounts[0].signer_key().is_none() {
|
||||
return Err(ProgramError::MissingRequiredSignature);
|
||||
}
|
||||
trace!("apply signature");
|
||||
apply_signature(&mut budget_state, keyed_accounts)
|
||||
.map_err(|e| ProgramError::CustomError(serialize(&e).unwrap()))?;
|
||||
trace!("apply signature committed");
|
||||
budget_state.serialize(&mut keyed_accounts[1].account.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_instruction(
|
||||
_program_id: &Pubkey,
|
||||
keyed_accounts: &mut [KeyedAccount],
|
||||
data: &[u8],
|
||||
) -> Result<(), BudgetError> {
|
||||
let instruction = deserialize(data).map_err(|err| {
|
||||
info!("Invalid transaction data: {:?} {:?}", data, err);
|
||||
BudgetError::AccountDataDeserializeFailure
|
||||
})?;
|
||||
|
||||
trace!("process_instruction: {:?}", instruction);
|
||||
apply_debits(keyed_accounts, &instruction)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
@ -162,12 +149,12 @@ mod test {
|
|||
use solana_sdk::hash::Hash;
|
||||
use solana_sdk::signature::{Keypair, KeypairUtil};
|
||||
use solana_sdk::system_program;
|
||||
use solana_sdk::transaction::Transaction;
|
||||
use solana_sdk::transaction::{InstructionError, Transaction, TransactionError};
|
||||
|
||||
fn process_transaction(
|
||||
tx: &Transaction,
|
||||
tx_accounts: &mut Vec<Account>,
|
||||
) -> Result<(), BudgetError> {
|
||||
) -> Result<(), TransactionError> {
|
||||
runtime::process_transaction(tx, tx_accounts, process_instruction)
|
||||
}
|
||||
|
||||
|
@ -219,7 +206,10 @@ mod test {
|
|||
// Ensure the transaction fails because of the unsigned key.
|
||||
assert_eq!(
|
||||
process_transaction(&tx, &mut accounts),
|
||||
Err(BudgetError::UnsignedKey)
|
||||
Err(TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::ProgramError(ProgramError::MissingRequiredSignature)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -255,7 +245,10 @@ mod test {
|
|||
// Ensure the transaction fails because of the unsigned key.
|
||||
assert_eq!(
|
||||
process_transaction(&tx, &mut accounts),
|
||||
Err(BudgetError::UnsignedKey)
|
||||
Err(TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::ProgramError(ProgramError::MissingRequiredSignature)
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -295,8 +288,13 @@ mod test {
|
|||
Hash::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
process_transaction(&tx, &mut accounts),
|
||||
Err(BudgetError::DestinationMissing)
|
||||
process_transaction(&tx, &mut accounts).unwrap_err(),
|
||||
TransactionError::InstructionError(
|
||||
0,
|
||||
InstructionError::ProgramError(ProgramError::CustomError(
|
||||
serialize(&BudgetError::DestinationMissing).unwrap()
|
||||
))
|
||||
)
|
||||
);
|
||||
assert_eq!(accounts[from_account].lamports, 0);
|
||||
assert_eq!(accounts[contract_account].lamports, 1);
|
||||
|
@ -323,10 +321,7 @@ mod test {
|
|||
assert!(!budget_state.is_pending());
|
||||
|
||||
// try to replay the timestamp contract
|
||||
assert_eq!(
|
||||
process_transaction(&tx, &mut accounts),
|
||||
Err(BudgetError::ContractNotPending)
|
||||
);
|
||||
process_transaction(&tx, &mut accounts).unwrap();
|
||||
assert_eq!(accounts[from_account].lamports, 0);
|
||||
assert_eq!(accounts[contract_account].lamports, 0);
|
||||
assert_eq!(accounts[to_account].lamports, 1);
|
||||
|
@ -391,10 +386,7 @@ mod test {
|
|||
&from.pubkey(),
|
||||
Hash::default(),
|
||||
);
|
||||
assert_eq!(
|
||||
process_transaction(&tx, &mut accounts),
|
||||
Err(BudgetError::ContractNotPending)
|
||||
);
|
||||
process_transaction(&tx, &mut accounts).unwrap();
|
||||
assert_eq!(accounts[from_account].lamports, 1);
|
||||
assert_eq!(accounts[contract_account].lamports, 0);
|
||||
assert_eq!(accounts.get(pay_account), None);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
mod budget_program;
|
||||
|
||||
use crate::budget_program::process_instruction;
|
||||
use bincode::serialize;
|
||||
use log::*;
|
||||
use solana_sdk::account::KeyedAccount;
|
||||
use solana_sdk::native_program::ProgramError;
|
||||
|
@ -20,5 +19,4 @@ fn entrypoint(
|
|||
trace!("process_instruction: {:?}", data);
|
||||
trace!("keyed_accounts: {:?}", keyed_accounts);
|
||||
process_instruction(program_id, keyed_accounts, data)
|
||||
.map_err(|e| ProgramError::CustomError(serialize(&e).unwrap()))
|
||||
}
|
||||
|
|
|
@ -2,19 +2,11 @@
|
|||
use crate::budget_expr::BudgetExpr;
|
||||
use bincode::{self, deserialize, serialize_into};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use solana_sdk::native_program::ProgramError;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub enum BudgetError {
|
||||
InsufficientFunds,
|
||||
ContractAlreadyExists,
|
||||
ContractNotPending,
|
||||
SourceIsPendingContract,
|
||||
UninitializedContract,
|
||||
DestinationMissing,
|
||||
FailedWitness,
|
||||
AccountDataTooSmall,
|
||||
AccountDataDeserializeFailure,
|
||||
UnsignedKey,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
|
||||
|
@ -35,14 +27,12 @@ impl BudgetState {
|
|||
self.pending_budget.is_some()
|
||||
}
|
||||
|
||||
pub fn serialize(&self, output: &mut [u8]) -> Result<(), BudgetError> {
|
||||
serialize_into(output, self).map_err(|err| match *err {
|
||||
_ => BudgetError::AccountDataTooSmall,
|
||||
})
|
||||
pub fn serialize(&self, output: &mut [u8]) -> Result<(), ProgramError> {
|
||||
serialize_into(output, self).map_err(|_| ProgramError::AccountDataTooSmall)
|
||||
}
|
||||
|
||||
pub fn deserialize(input: &[u8]) -> bincode::Result<Self> {
|
||||
deserialize(input)
|
||||
pub fn deserialize(input: &[u8]) -> Result<Self, ProgramError> {
|
||||
deserialize(input).map_err(|_| ProgramError::InvalidAccountData)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -67,7 +57,7 @@ mod test {
|
|||
let b = BudgetState::default();
|
||||
assert_eq!(
|
||||
b.serialize(&mut a.data),
|
||||
Err(BudgetError::AccountDataTooSmall)
|
||||
Err(ProgramError::AccountDataTooSmall)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,19 +189,20 @@ pub fn execute_transaction(
|
|||
|
||||
/// A utility function for unit-tests. Same as execute_transaction(), but bypasses the loaders
|
||||
/// for easier usage and better stack traces.
|
||||
pub fn process_transaction<F, E>(
|
||||
pub fn process_transaction<F>(
|
||||
tx: &Transaction,
|
||||
tx_accounts: &mut Vec<Account>,
|
||||
process_instruction: F,
|
||||
) -> Result<(), E>
|
||||
) -> Result<(), TransactionError>
|
||||
where
|
||||
F: Fn(&Pubkey, &mut [KeyedAccount], &[u8]) -> Result<(), E>,
|
||||
F: Fn(&Pubkey, &mut [KeyedAccount], &[u8]) -> Result<(), ProgramError>,
|
||||
{
|
||||
for _ in tx_accounts.len()..tx.account_keys.len() {
|
||||
tx_accounts.push(Account::new(0, 0, &system_program::id()));
|
||||
}
|
||||
for (i, ix) in tx.instructions.iter().enumerate() {
|
||||
let mut ix_accounts = get_subset_unchecked_mut(tx_accounts, &ix.accounts).unwrap();
|
||||
let mut ix_accounts = get_subset_unchecked_mut(tx_accounts, &ix.accounts)
|
||||
.map_err(|err| TransactionError::InstructionError(i as u8, err))?;
|
||||
let mut keyed_accounts: Vec<_> = ix
|
||||
.accounts
|
||||
.iter()
|
||||
|
@ -215,12 +216,14 @@ where
|
|||
.collect();
|
||||
|
||||
let program_id = tx.program_id(i);
|
||||
if system_program::check_id(&program_id) {
|
||||
let result = if system_program::check_id(&program_id) {
|
||||
crate::system_program::entrypoint(&program_id, &mut keyed_accounts, &ix.data, 0)
|
||||
.unwrap();
|
||||
} else {
|
||||
process_instruction(&program_id, &mut keyed_accounts, &ix.data)?;
|
||||
}
|
||||
process_instruction(&program_id, &mut keyed_accounts, &ix.data)
|
||||
};
|
||||
result.map_err(|err| {
|
||||
TransactionError::InstructionError(i as u8, InstructionError::ProgramError(err))
|
||||
})?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -26,6 +26,12 @@ pub enum ProgramError {
|
|||
/// A signature was required but not found
|
||||
MissingRequiredSignature,
|
||||
|
||||
/// An initialize instruction was sent to an account that has already been initialized.
|
||||
AccountAlreadyInitialized,
|
||||
|
||||
/// An attempt to operate on an account that hasn't been initialized.
|
||||
UninitializedAccount,
|
||||
|
||||
/// CustomError allows on-chain programs to implement program-specific error types and see
|
||||
/// them returned by the Solana runtime. A CustomError may be any type that is serialized
|
||||
/// to a Vec of bytes, max length 32 bytes. Any CustomError Vec greater than this length will
|
||||
|
|
Loading…
Reference in New Issue