use quick_protobuf::deserialize_from_slice;
use solana_program::account_info::AccountInfo;
use solana_program::program_error::ProgramError;
use switchboard_protos::protos::aggregator_state::mod_AggregatorState;
use solana_program::pubkey::Pubkey;
pub use switchboard_protos::protos::aggregator_state::AggregatorState;
pub use switchboard_protos::protos::aggregator_state::RoundResult;
pub fn is_current_round_valid(aggregator: &AggregatorState) -> Result<bool, ProgramError> {
let maybe_round = aggregator.current_round_result.clone();
if maybe_round.is_none() {
return Ok(false);
}
let round = maybe_round.unwrap();
let configs = aggregator.configs.as_ref().ok_or(ProgramError::InvalidAccountData)?;
if round.num_success < configs.min_confirmations {
return Ok(false);
}
let max_resp: f64 = 1.0 * round.min_response.abs().max(round.max_response.abs());
let min_resp: f64 = 1.0 * round.min_response.abs().min(round.max_response.abs());
if min_resp / max_resp * 100.0 < configs.min_round_agreement_percentage.into() {
return Ok(false);
}
Ok(true)
}
pub fn get_aggregator<'a>(switchboard_feed: &'a AccountInfo<'a>) -> Result<AggregatorState, ProgramError> {
let state_buffer = switchboard_feed.try_borrow_data()?;
let aggregator_state: AggregatorState =
deserialize_from_slice(&state_buffer[1..]).map_err(|_| ProgramError::InvalidAccountData)?;
Ok(aggregator_state)
}
pub fn get_aggregator_result<'a>(aggregator: &AggregatorState) -> Result<RoundResult, ProgramError> {
let mut maybe_round = aggregator.current_round_result.clone();
if !is_current_round_valid(&aggregator)? {
maybe_round = aggregator.last_round_result.clone();
}
maybe_round.ok_or(ProgramError::InvalidAccountData)
}
#[cfg(test)]
mod tests {
use super::*;
pub fn new_account_info<'a>(
owner: &'a Pubkey,
key: &'a Pubkey,
lamports: &'a mut u64,
data: &'a mut [u8],
) -> AccountInfo<'a> {
AccountInfo::new(
key,
false,
true,
lamports,
data,
owner,
false,
100,
)
}
pub fn create_aggregator(current_round: RoundResult, last_round: RoundResult) -> AggregatorState {
AggregatorState {
version: 1,
configs: Some(mod_AggregatorState::Configs {
min_confirmations: 10,
min_update_delay_seconds: 10,
min_round_agreement_percentage: 95,
locked: false,
}),
fulfillment_manager_pubkey: Vec::new(),
job_definition_pubkeys: Vec::new(),
agreement: None,
current_round_result: Some(current_round),
last_round_result: Some(last_round),
}
}
#[test]
fn test_reject_current_on_variance() {
let current_round = RoundResult {
num_success: 20,
num_error: 5,
mean: 50.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: 25.0,
max_response: 75.0
};
let last_round = RoundResult {
num_success: 30,
num_error: 0,
mean: 100.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: 50.0,
max_response: 150.0
};
let aggregator = create_aggregator(current_round, last_round.clone());
assert_eq!(get_aggregator_result(&aggregator).unwrap(), last_round.clone());
}
#[test]
fn test_accept_current_on_variance() {
let current_round = RoundResult {
num_success: 20,
num_error: 5,
mean: 97.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: 96.0,
max_response: 100.0
};
let last_round = RoundResult {
num_success: 30,
num_error: 0,
mean: 100.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: 50.0,
max_response: 150.0
};
let aggregator = create_aggregator(current_round.clone(), last_round.clone());
assert_eq!(get_aggregator_result(&aggregator).unwrap(), current_round.clone());
}
#[test]
fn test_reject_current_on_variance_negative() {
let current_round = RoundResult {
num_success: 20,
num_error: 5,
mean: -50.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: -25.0,
max_response: -75.0,
};
let last_round = RoundResult {
num_success: 30,
num_error: 0,
mean: -100.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: -150.0,
max_response: -50.0,
};
let aggregator = create_aggregator(current_round, last_round.clone());
assert_eq!(get_aggregator_result(&aggregator).unwrap(), last_round.clone());
}
#[test]
fn test_accept_current_on_variance_negative() {
let current_round = RoundResult {
num_success: 20,
num_error: 5,
mean: -97.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: -96.0,
max_response: -100.0,
};
let last_round = RoundResult {
num_success: 30,
num_error: 0,
mean: -100.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: -150.0,
max_response: -50.0,
};
let aggregator = create_aggregator(current_round.clone(), last_round.clone());
assert_eq!(get_aggregator_result(&aggregator).unwrap(), current_round.clone());
}
#[test]
fn test_reject_current_on_sucess_count() {
let current_round = RoundResult {
num_success: 2,
num_error: 5,
mean: 97.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: 96.0,
max_response: 100.0
};
let last_round = RoundResult {
num_success: 30,
num_error: 0,
mean: 100.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: 100.0,
max_response: 100.0
};
let aggregator = create_aggregator(current_round.clone(), last_round.clone());
assert_eq!(get_aggregator_result(&aggregator).unwrap(), last_round.clone());
}
#[test]
fn test_accept_current_on_sucess_count() {
let current_round = RoundResult {
num_success: 20,
num_error: 5,
mean: 97.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: 96.0,
max_response: 100.0
};
let last_round = RoundResult {
num_success: 30,
num_error: 0,
mean: 100.0,
round_open_slot: 1,
round_open_timestamp: 1,
min_response: 100.0,
max_response: 100.0
};
let aggregator = create_aggregator(current_round.clone(), last_round.clone());
assert_eq!(get_aggregator_result(&aggregator).unwrap(), current_round.clone());
}
}