868 lines
36 KiB
Rust
868 lines
36 KiB
Rust
// file: kb-lib/src/materializer/staking/core.rs
|
|
// version: 7
|
|
|
|
//! Instruction-level staking projections for native Stake and Vote observations.
|
|
|
|
const ACCEPTED_FAMILIES: &[crate::EventFamily] = &[
|
|
crate::EventFamily::Lifecycle,
|
|
crate::EventFamily::Admin,
|
|
crate::EventFamily::Staking,
|
|
crate::EventFamily::Reward,
|
|
];
|
|
const STAKE_SURFACE: &str = "solana_native_stake";
|
|
const VOTE_SURFACE: &str = "solana_native_vote";
|
|
const STAKE_ACCOUNT_ENTRIES: &[&str] = &[
|
|
"initialize",
|
|
"initialize_checked",
|
|
"delegate_stake",
|
|
"deactivate",
|
|
"deactivate_delinquent",
|
|
];
|
|
const STAKE_VALUE_ENTRIES: &[&str] = &["split", "withdraw", "merge", "move_stake", "move_lamports"];
|
|
const STAKE_AUTHORITY_ENTRIES: &[&str] = &[
|
|
"authorize",
|
|
"authorize_with_seed",
|
|
"authorize_checked",
|
|
"authorize_checked_with_seed",
|
|
];
|
|
const STAKE_LOCKUP_ENTRIES: &[&str] = &["set_lockup", "set_lockup_checked"];
|
|
const VOTE_ACCOUNT_ENTRIES: &[&str] = &["initialize_account", "initialize_account_v2"];
|
|
const VOTE_AUTHORITY_ENTRIES: &[&str] = &[
|
|
"authorize",
|
|
"authorize_checked",
|
|
"authorize_with_seed",
|
|
"authorize_checked_with_seed",
|
|
];
|
|
const VOTE_ADMIN_ENTRIES: &[&str] = &[
|
|
"update_validator_identity",
|
|
"update_commission",
|
|
"update_commission_collector",
|
|
"update_commission_bps",
|
|
];
|
|
const VOTE_STATE_ENTRIES: &[&str] = &[
|
|
"vote",
|
|
"vote_switch",
|
|
"update_vote_state",
|
|
"update_vote_state_switch",
|
|
"compact_update_vote_state",
|
|
"compact_update_vote_state_switch",
|
|
"tower_sync",
|
|
"tower_sync_switch",
|
|
];
|
|
const VOTE_VALUE_ENTRIES: &[&str] = &["withdraw"];
|
|
const VOTE_REWARD_ENTRIES: &[&str] = &["deposit_delegator_rewards"];
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
enum ProjectionKind {
|
|
StakeAccount,
|
|
StakeValue,
|
|
StakeAuthority,
|
|
StakeLockup,
|
|
VoteAccount,
|
|
VoteAuthority,
|
|
VoteAdmin,
|
|
VoteState,
|
|
VoteValue,
|
|
VoteReward,
|
|
}
|
|
|
|
impl ProjectionKind {
|
|
fn domain(self) -> &'static str {
|
|
return match self {
|
|
Self::StakeAccount => "stake_account",
|
|
Self::StakeValue => "stake_value",
|
|
Self::StakeAuthority => "stake_authority",
|
|
Self::StakeLockup => "stake_lockup",
|
|
Self::VoteAccount => "vote_account",
|
|
Self::VoteAuthority => "vote_authority",
|
|
Self::VoteAdmin => "vote_admin",
|
|
Self::VoteState => "vote_state",
|
|
Self::VoteValue => "vote_value",
|
|
Self::VoteReward => "vote_reward",
|
|
};
|
|
}
|
|
|
|
fn family(self) -> crate::MaterializedEventFamily {
|
|
return match self {
|
|
Self::VoteReward => crate::MaterializedEventFamily::Reward,
|
|
_ => crate::MaterializedEventFamily::Staking,
|
|
};
|
|
}
|
|
|
|
fn output_key(self, operation: &str) -> std::string::String {
|
|
return format!("{}:{operation}:0", self.domain());
|
|
}
|
|
}
|
|
|
|
/// Stable staking materializer for committed native Stake and Vote observations.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct StakingMaterializer;
|
|
|
|
impl crate::Materializer for crate::StakingMaterializer {
|
|
fn materializer_name(&self) -> &'static str {
|
|
return "kb_materializer_staking";
|
|
}
|
|
|
|
fn materializer_version(&self) -> &'static str {
|
|
return env!("CARGO_PKG_VERSION");
|
|
}
|
|
|
|
fn accepts_event(&self, event: &crate::DecodedProtocolEvent) -> bool {
|
|
return family_is_accepted(event.event_family)
|
|
&& projection(event.surface_code.0.as_str(), event.event_name.0.as_str()).is_some();
|
|
}
|
|
|
|
fn materialize_event(
|
|
&self,
|
|
event: &crate::DecodedProtocolEvent,
|
|
) -> kb_core::Result<std::vec::Vec<crate::MaterializedEvent>> {
|
|
if !crate::Materializer::accepts_event(self, event) {
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
}
|
|
return std::result::Result::Ok(std::vec::Vec::new());
|
|
}
|
|
}
|
|
|
|
impl crate::EventMaterializer for crate::StakingMaterializer {
|
|
fn identity(&self) -> crate::MaterializerIdentity {
|
|
return crate::MaterializerIdentity {
|
|
name: "solana_native_staking".to_string(),
|
|
version: env!("CARGO_PKG_VERSION").to_string(),
|
|
};
|
|
}
|
|
|
|
fn accepted_families(&self) -> &'static [crate::EventFamily] {
|
|
return ACCEPTED_FAMILIES;
|
|
}
|
|
|
|
fn accepts_observation(&self, observation: &crate::DecodedObservation) -> bool {
|
|
return family_is_accepted(observation.event.event_family)
|
|
&& projection(
|
|
observation.event.surface_code.0.as_str(),
|
|
observation.event.event_name.0.as_str(),
|
|
)
|
|
.is_some();
|
|
}
|
|
|
|
fn transaction_policy(
|
|
&self,
|
|
_family: crate::EventFamily,
|
|
) -> crate::MaterializationTransactionPolicy {
|
|
return crate::MaterializationTransactionPolicy::SuccessfulCommittedOnly;
|
|
}
|
|
|
|
fn materialize(
|
|
&self,
|
|
observation: &crate::DecodedObservation,
|
|
) -> crate::MaterializerExecutionResult {
|
|
let projection = match projection(
|
|
observation.event.surface_code.0.as_str(),
|
|
observation.event.event_name.0.as_str(),
|
|
) {
|
|
std::option::Option::Some(value) => value,
|
|
std::option::Option::None => {
|
|
tracing::debug!(
|
|
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
|
|
action = "materialize_staking",
|
|
surface_code = %observation.event.surface_code.0.as_str(),
|
|
entry_code = %observation.event.event_name.0.as_str(),
|
|
accepted = false,
|
|
"ignore observation outside supported native staking projections"
|
|
);
|
|
return crate::MaterializerExecutionResult::ignored();
|
|
},
|
|
};
|
|
if !family_is_accepted(observation.event.event_family) {
|
|
return crate::MaterializerExecutionResult::ignored();
|
|
}
|
|
if observation.transaction_failed || !observation.observation_committed {
|
|
tracing::debug!(
|
|
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
|
|
action = "materialize_staking",
|
|
surface_code = %observation.event.surface_code.0.as_str(),
|
|
entry_code = %observation.event.event_name.0.as_str(),
|
|
committed = observation.observation_committed,
|
|
transaction_failed = observation.transaction_failed,
|
|
"refuse uncommitted native staking observation"
|
|
);
|
|
return crate::MaterializerExecutionResult::refused(
|
|
"failed_transaction_staking_refused",
|
|
"failed or uncommitted native Stake/Vote observations cannot create staking outputs",
|
|
);
|
|
}
|
|
let accounts = observation
|
|
.payload_json
|
|
.get("accounts")
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null);
|
|
let parameters = observation
|
|
.payload_json
|
|
.get("parameters")
|
|
.cloned()
|
|
.unwrap_or(serde_json::Value::Null);
|
|
let operation = observation.event.event_name.0.clone();
|
|
let output = crate::MaterializedOutput {
|
|
output_key: projection.output_key(operation.as_str()),
|
|
family: projection.family(),
|
|
payload_json: serde_json::json!({
|
|
"projectionVersion": 1,
|
|
"projectionSemantics": "committed_instruction_staking_event",
|
|
"domain": projection.domain(),
|
|
"operation": operation,
|
|
"programId": observation.event.program_id.0.clone(),
|
|
"signature": observation.event.signature.0.clone(),
|
|
"slot": observation.event.slot.0,
|
|
"instructionPath": observation.event.instruction_path.0.clone(),
|
|
"transactionSucceeded": true,
|
|
"accounts": accounts,
|
|
"parameters": parameters,
|
|
"projection": projection_details(
|
|
projection,
|
|
&accounts,
|
|
¶meters,
|
|
observation.event.event_name.0.as_str(),
|
|
),
|
|
}),
|
|
};
|
|
tracing::debug!(
|
|
target: crate::MATERIALIZER_STAKING_TRACING_TARGET,
|
|
action = "materialize_staking",
|
|
surface_code = %observation.event.surface_code.0.as_str(),
|
|
entry_code = %observation.event.event_name.0.as_str(),
|
|
projection_domain = projection.domain(),
|
|
output_count = 1_usize,
|
|
"materialize committed native staking observation"
|
|
);
|
|
return crate::MaterializerExecutionResult {
|
|
status: crate::MaterializerOutcomeStatus::Inserted,
|
|
outputs: std::vec![output],
|
|
diagnostics: std::vec::Vec::new(),
|
|
};
|
|
}
|
|
}
|
|
|
|
fn family_is_accepted(family: crate::EventFamily) -> bool {
|
|
return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family);
|
|
}
|
|
|
|
fn projection(surface_code: &str, entry_code: &str) -> std::option::Option<ProjectionKind> {
|
|
if surface_code == STAKE_SURFACE {
|
|
return stake_projection(entry_code);
|
|
}
|
|
if surface_code == VOTE_SURFACE {
|
|
return vote_projection(entry_code);
|
|
}
|
|
return std::option::Option::None;
|
|
}
|
|
|
|
fn stake_projection(entry_code: &str) -> std::option::Option<ProjectionKind> {
|
|
if contains(STAKE_ACCOUNT_ENTRIES, entry_code) {
|
|
return std::option::Option::Some(ProjectionKind::StakeAccount);
|
|
}
|
|
if contains(STAKE_VALUE_ENTRIES, entry_code) {
|
|
return std::option::Option::Some(ProjectionKind::StakeValue);
|
|
}
|
|
if contains(STAKE_AUTHORITY_ENTRIES, entry_code) {
|
|
return std::option::Option::Some(ProjectionKind::StakeAuthority);
|
|
}
|
|
if contains(STAKE_LOCKUP_ENTRIES, entry_code) {
|
|
return std::option::Option::Some(ProjectionKind::StakeLockup);
|
|
}
|
|
return std::option::Option::None;
|
|
}
|
|
|
|
fn vote_projection(entry_code: &str) -> std::option::Option<ProjectionKind> {
|
|
if contains(VOTE_ACCOUNT_ENTRIES, entry_code) {
|
|
return std::option::Option::Some(ProjectionKind::VoteAccount);
|
|
}
|
|
if contains(VOTE_AUTHORITY_ENTRIES, entry_code) {
|
|
return std::option::Option::Some(ProjectionKind::VoteAuthority);
|
|
}
|
|
if contains(VOTE_ADMIN_ENTRIES, entry_code) {
|
|
return std::option::Option::Some(ProjectionKind::VoteAdmin);
|
|
}
|
|
if contains(VOTE_STATE_ENTRIES, entry_code) {
|
|
return std::option::Option::Some(ProjectionKind::VoteState);
|
|
}
|
|
if contains(VOTE_VALUE_ENTRIES, entry_code) {
|
|
return std::option::Option::Some(ProjectionKind::VoteValue);
|
|
}
|
|
if contains(VOTE_REWARD_ENTRIES, entry_code) {
|
|
return std::option::Option::Some(ProjectionKind::VoteReward);
|
|
}
|
|
return std::option::Option::None;
|
|
}
|
|
|
|
fn projection_details(
|
|
projection: ProjectionKind,
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
operation: &str,
|
|
) -> serde_json::Value {
|
|
return match projection {
|
|
ProjectionKind::StakeAccount => stake_account_details(accounts, parameters, operation),
|
|
ProjectionKind::StakeValue => stake_value_details(accounts, parameters, operation),
|
|
ProjectionKind::StakeAuthority => stake_authority_details(accounts, parameters, operation),
|
|
ProjectionKind::StakeLockup => stake_lockup_details(accounts, parameters, operation),
|
|
ProjectionKind::VoteAccount => vote_account_details(accounts, parameters, operation),
|
|
ProjectionKind::VoteAuthority => vote_authority_details(accounts, parameters, operation),
|
|
ProjectionKind::VoteAdmin => vote_admin_details(accounts, parameters, operation),
|
|
ProjectionKind::VoteState => vote_state_details(accounts, parameters, operation),
|
|
ProjectionKind::VoteValue => vote_value_details(accounts, parameters, operation),
|
|
ProjectionKind::VoteReward => vote_reward_details(accounts, parameters, operation),
|
|
};
|
|
}
|
|
|
|
fn stake_account_details(
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
operation: &str,
|
|
) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"stakeAccount": account_key_for_role(accounts, "stake_account"),
|
|
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
|
"authorized": parameters.get("authorized").cloned().unwrap_or(serde_json::Value::Null),
|
|
"lockup": parameters.get("lockup").cloned().unwrap_or(serde_json::Value::Null),
|
|
"activationStateComputed": false,
|
|
"transactionFinalAccountStateCaptured": false,
|
|
"stateTransition": match operation {
|
|
"initialize" | "initialize_checked" => "stake_account_initialized",
|
|
"delegate_stake" => "stake_delegation_requested",
|
|
"deactivate" => "stake_deactivation_requested",
|
|
"deactivate_delinquent" => "stake_deactivation_requested_for_delinquent_vote_account",
|
|
_ => "stake_account_state_transition_requested",
|
|
},
|
|
});
|
|
}
|
|
|
|
fn stake_value_details(
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
operation: &str,
|
|
) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"stakeAccount": account_key_for_role(accounts, "stake_account"),
|
|
"splitAccount": account_key_for_role(accounts, "split_account"),
|
|
"destinationAccount": first_account_key_for_roles(accounts, &["destination", "recipient_account", "destination_stake_account"]),
|
|
"sourceStakeAccount": account_key_for_role(accounts, "source_stake_account"),
|
|
"lamports": parameters.get("lamports").cloned().unwrap_or(serde_json::Value::Null),
|
|
"lamportBalanceChangesDuplicated": false,
|
|
"activationStateComputed": false,
|
|
"transactionFinalAccountStateCaptured": false,
|
|
"stateTransition": match operation {
|
|
"split" => "stake_split_requested",
|
|
"withdraw" => "stake_lamports_withdrawn_or_account_closed_conditionally",
|
|
"merge" => "stake_accounts_merged",
|
|
"move_stake" => "active_stake_moved",
|
|
"move_lamports" => "inactive_lamports_moved",
|
|
_ => "stake_value_transition_requested",
|
|
},
|
|
});
|
|
}
|
|
|
|
fn stake_authority_details(
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
operation: &str,
|
|
) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"stakeAccount": account_key_for_role(accounts, "stake_account"),
|
|
"currentAuthority": first_account_key_for_roles(accounts, &["stake_or_withdraw_authority", "current_authority", "stake_authority", "withdraw_authority", "authority_base"]),
|
|
"newAuthority": first_parameter_or_account(
|
|
parameters,
|
|
&["newAuthority", "newAuthorizedPubkey"],
|
|
accounts,
|
|
"new_authority",
|
|
),
|
|
"authorizationType": parameters.get("authorizationType").cloned().unwrap_or(serde_json::Value::Null),
|
|
"authoritySeed": parameters.get("authoritySeed").cloned().unwrap_or(serde_json::Value::Null),
|
|
"authorityOwner": parameters.get("authorityOwner").cloned().unwrap_or(serde_json::Value::Null),
|
|
"stateTransition": match operation {
|
|
"authorize_with_seed" | "authorize_checked_with_seed" => "stake_authority_changed_with_seed",
|
|
"authorize_checked" => "stake_authority_changed_with_new_authority_signature",
|
|
_ => "stake_authority_changed",
|
|
},
|
|
"transactionFinalAccountStateCaptured": false,
|
|
});
|
|
}
|
|
|
|
fn stake_lockup_details(
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
operation: &str,
|
|
) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"stakeAccount": account_key_for_role(accounts, "stake_account"),
|
|
"custodian": first_account_key_for_roles(accounts, &["lockup_custodian", "lockup_or_withdraw_authority", "new_lockup_authority"]),
|
|
"lockup": parameters.clone(),
|
|
"checkedCustodianFromAccount": operation == "set_lockup_checked",
|
|
"stateTransition": "stake_lockup_updated",
|
|
"transactionFinalAccountStateCaptured": false,
|
|
});
|
|
}
|
|
|
|
fn vote_account_details(
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
operation: &str,
|
|
) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
|
"validatorIdentity": account_key_for_role(accounts, "validator_identity"),
|
|
"authorizedVoter": parameters.get("authorizedVoter").cloned().unwrap_or(serde_json::Value::Null),
|
|
"authorizedWithdrawer": parameters.get("authorizedWithdrawer").cloned().unwrap_or(serde_json::Value::Null),
|
|
"commissionPercent": parameters.get("commissionPercent").cloned().unwrap_or(serde_json::Value::Null),
|
|
"inflationRewardsCommissionBasisPoints": parameters.get("inflationRewardsCommissionBasisPoints").cloned().unwrap_or(serde_json::Value::Null),
|
|
"blockRevenueCommissionBasisPoints": parameters.get("blockRevenueCommissionBasisPoints").cloned().unwrap_or(serde_json::Value::Null),
|
|
"inflationRewardsCollector": account_key_for_role(accounts, "inflation_rewards_collector"),
|
|
"blockRevenueCollector": account_key_for_role(accounts, "block_revenue_collector"),
|
|
"stateTransition": if operation == "initialize_account_v2" {
|
|
"vote_account_initialized_v2"
|
|
} else {
|
|
"vote_account_initialized"
|
|
},
|
|
"transactionFinalAccountStateCaptured": false,
|
|
});
|
|
}
|
|
|
|
fn vote_authority_details(
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
operation: &str,
|
|
) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
|
"currentAuthority": first_account_key_for_roles(accounts, &["current_authority", "current_authority_base"]),
|
|
"newAuthority": first_parameter_or_account(
|
|
parameters,
|
|
&["newAuthority"],
|
|
accounts,
|
|
"new_authority",
|
|
),
|
|
"authorizationType": parameters.get("authorizationType").cloned().unwrap_or(serde_json::Value::Null),
|
|
"currentAuthorityDerivedKeyOwner": parameters.get("currentAuthorityDerivedKeyOwner").cloned().unwrap_or(serde_json::Value::Null),
|
|
"currentAuthorityDerivedKeySeed": parameters.get("currentAuthorityDerivedKeySeed").cloned().unwrap_or(serde_json::Value::Null),
|
|
"newAuthorityFromAccount": parameters.get("newAuthorityFromAccount").cloned().unwrap_or(serde_json::Value::Bool(false)),
|
|
"stateTransition": match operation {
|
|
"authorize_with_seed" | "authorize_checked_with_seed" => "vote_authority_changed_with_seed",
|
|
"authorize_checked" => "vote_authority_changed_with_new_authority_signature",
|
|
_ => "vote_authority_changed",
|
|
},
|
|
"transactionFinalAccountStateCaptured": false,
|
|
});
|
|
}
|
|
|
|
fn vote_admin_details(
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
operation: &str,
|
|
) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
|
"withdrawAuthority": account_key_for_role(accounts, "withdraw_authority"),
|
|
"newValidatorIdentity": account_key_for_role(accounts, "new_validator_identity"),
|
|
"newCommissionCollector": account_key_for_role(accounts, "new_commission_collector"),
|
|
"commissionPercent": parameters.get("commissionPercent").cloned().unwrap_or(serde_json::Value::Null),
|
|
"commissionBasisPoints": parameters.get("commissionBasisPoints").cloned().unwrap_or(serde_json::Value::Null),
|
|
"commissionKind": parameters.get("commissionKind").cloned().unwrap_or(serde_json::Value::Null),
|
|
"stateTransition": match operation {
|
|
"update_validator_identity" => "vote_validator_identity_updated",
|
|
"update_commission" | "update_commission_bps" => "vote_commission_updated",
|
|
"update_commission_collector" => "vote_commission_collector_updated",
|
|
_ => "vote_admin_updated",
|
|
},
|
|
"transactionFinalAccountStateCaptured": false,
|
|
});
|
|
}
|
|
|
|
fn vote_state_details(
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
operation: &str,
|
|
) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
|
"voteAuthority": account_key_for_role(accounts, "vote_authority"),
|
|
"lastVotedSlot": parameters.get("lastVotedSlot").cloned().unwrap_or(serde_json::Value::Null),
|
|
"slots": parameters.get("slots").cloned().unwrap_or(serde_json::Value::Null),
|
|
"root": parameters.get("root").cloned().unwrap_or(serde_json::Value::Null),
|
|
"hash": first_parameter_value(parameters, &["hash", "bankHash"]),
|
|
"timestamp": parameters.get("timestamp").cloned().unwrap_or(serde_json::Value::Null),
|
|
"switchProofHash": parameters.get("switchProofHash").cloned().unwrap_or(serde_json::Value::Null),
|
|
"stateTransition": if operation.contains("switch") {
|
|
"vote_state_updated_with_switch_proof"
|
|
} else if operation.starts_with("tower_sync") {
|
|
"vote_tower_synchronized"
|
|
} else {
|
|
"vote_state_updated"
|
|
},
|
|
"voteCreditsComputed": false,
|
|
"transactionFinalAccountStateCaptured": false,
|
|
});
|
|
}
|
|
|
|
fn vote_value_details(
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
_operation: &str,
|
|
) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
|
"recipientAccount": account_key_for_role(accounts, "recipient_account"),
|
|
"withdrawAuthority": account_key_for_role(accounts, "withdraw_authority"),
|
|
"lamports": parameters.get("lamports").cloned().unwrap_or(serde_json::Value::Null),
|
|
"lamportBalanceChangesDuplicated": false,
|
|
"stateTransition": "vote_lamports_withdrawn_or_account_closed_conditionally",
|
|
"transactionFinalAccountStateCaptured": false,
|
|
});
|
|
}
|
|
|
|
fn vote_reward_details(
|
|
accounts: &serde_json::Value,
|
|
parameters: &serde_json::Value,
|
|
_operation: &str,
|
|
) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"voteAccount": account_key_for_role(accounts, "vote_account"),
|
|
"depositSource": account_key_for_role(accounts, "deposit_source"),
|
|
"depositLamports": parameters.get("depositLamports").cloned().unwrap_or(serde_json::Value::Null),
|
|
"lamportBalanceChangesDuplicated": false,
|
|
"stateTransition": "delegator_rewards_deposited_into_vote_account",
|
|
"transactionFinalAccountStateCaptured": false,
|
|
});
|
|
}
|
|
|
|
fn first_parameter_or_account(
|
|
parameters: &serde_json::Value,
|
|
keys: &[&str],
|
|
accounts: &serde_json::Value,
|
|
fallback_role: &str,
|
|
) -> serde_json::Value {
|
|
let value = first_parameter_value(parameters, keys);
|
|
if !value.is_null() {
|
|
return value;
|
|
}
|
|
return account_key_for_role(accounts, fallback_role);
|
|
}
|
|
|
|
fn first_parameter_value(parameters: &serde_json::Value, keys: &[&str]) -> serde_json::Value {
|
|
for key in keys {
|
|
if let std::option::Option::Some(value) = parameters.get(*key) {
|
|
return value.clone();
|
|
}
|
|
}
|
|
return serde_json::Value::Null;
|
|
}
|
|
|
|
fn first_account_key_for_roles(accounts: &serde_json::Value, roles: &[&str]) -> serde_json::Value {
|
|
for role in roles {
|
|
let value = account_key_for_role(accounts, role);
|
|
if !value.is_null() {
|
|
return value;
|
|
}
|
|
}
|
|
return serde_json::Value::Null;
|
|
}
|
|
|
|
fn account_key_for_role(accounts: &serde_json::Value, role: &str) -> serde_json::Value {
|
|
let account_key = accounts.as_array().and_then(|values| {
|
|
return values.iter().find_map(|value| {
|
|
if value.get("role").and_then(serde_json::Value::as_str)
|
|
!= std::option::Option::Some(role)
|
|
{
|
|
return std::option::Option::None;
|
|
}
|
|
return value
|
|
.get("accountKey")
|
|
.and_then(serde_json::Value::as_str)
|
|
.map(|account_key| return account_key.to_string());
|
|
});
|
|
});
|
|
return match account_key {
|
|
std::option::Option::Some(value) => serde_json::Value::String(value),
|
|
std::option::Option::None => serde_json::Value::Null,
|
|
};
|
|
}
|
|
|
|
fn contains(entries: &[&str], entry_code: &str) -> bool {
|
|
return entries.iter().any(|entry| return *entry == entry_code);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
fn observation(
|
|
surface_code: &str,
|
|
entry_code: &str,
|
|
family: crate::EventFamily,
|
|
transaction_failed: bool,
|
|
accounts: serde_json::Value,
|
|
parameters: serde_json::Value,
|
|
) -> crate::DecodedObservation {
|
|
return crate::DecodedObservation {
|
|
event_key: format!("{entry_code}:0"),
|
|
event: crate::DecodedProtocolEvent {
|
|
signature: crate::Signature("signature".to_string()),
|
|
slot: crate::Slot(42),
|
|
instruction_path: crate::InstructionPath("0".to_string()),
|
|
program_id: crate::ProgramId("program-id".to_string()),
|
|
protocol_code: crate::ProtocolCode("solana_native".to_string()),
|
|
surface_code: crate::SurfaceCode(surface_code.to_string()),
|
|
event_code: crate::EventCode(format!("{surface_code}.{entry_code}")),
|
|
event_name: crate::EventName(entry_code.to_string()),
|
|
event_family: family,
|
|
source_kind: crate::EventSourceKind::Instruction,
|
|
confidence: crate::DecoderConfidence::ManualExact,
|
|
},
|
|
payload_json: serde_json::json!({"accounts": accounts, "parameters": parameters}),
|
|
transaction_failed,
|
|
transaction_error: if transaction_failed {
|
|
std::option::Option::Some(serde_json::json!({"InstructionError": [0, "Custom"]}))
|
|
} else {
|
|
std::option::Option::None
|
|
},
|
|
observation_committed: !transaction_failed,
|
|
proof: crate::DecoderProof {
|
|
kind: crate::DecoderProofKind::Manual,
|
|
confidence: crate::DecoderConfidence::ManualExact,
|
|
evidence: std::vec!["fixture".to_string()],
|
|
},
|
|
};
|
|
}
|
|
|
|
fn account(role: &str, account_key: &str) -> serde_json::Value {
|
|
return serde_json::json!({"role": role, "accountKey": account_key});
|
|
}
|
|
|
|
#[test]
|
|
fn stake_initialize_creates_instructional_staking_projection() {
|
|
let materializer = crate::StakingMaterializer;
|
|
let observation = observation(
|
|
super::STAKE_SURFACE,
|
|
"initialize",
|
|
crate::EventFamily::Lifecycle,
|
|
false,
|
|
serde_json::json!([account("stake_account", "stake111")]),
|
|
serde_json::json!({"authorized": {"staker": "staker111"}}),
|
|
);
|
|
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
|
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Inserted);
|
|
assert_eq!(result.outputs[0].family, crate::MaterializedEventFamily::Staking);
|
|
assert_eq!(result.outputs[0].output_key, "stake_account:initialize:0");
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["stakeAccount"], "stake111");
|
|
assert_eq!(
|
|
result.outputs[0].payload_json["projection"]["transactionFinalAccountStateCaptured"],
|
|
false
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stake_authority_and_lockup_events_are_projected() {
|
|
let materializer = crate::StakingMaterializer;
|
|
let authority = observation(
|
|
super::STAKE_SURFACE,
|
|
"authorize_with_seed",
|
|
crate::EventFamily::Admin,
|
|
false,
|
|
serde_json::json!([account("stake_account", "stake111")]),
|
|
serde_json::json!({
|
|
"newAuthorizedPubkey": "new111",
|
|
"authorizationType": "staker",
|
|
"authoritySeed": "seed",
|
|
"authorityOwner": "owner111",
|
|
}),
|
|
);
|
|
let lockup = observation(
|
|
super::STAKE_SURFACE,
|
|
"set_lockup_checked",
|
|
crate::EventFamily::Admin,
|
|
false,
|
|
serde_json::json!([
|
|
account("stake_account", "stake111"),
|
|
account("lockup_custodian", "custodian111"),
|
|
]),
|
|
serde_json::json!({"epoch": 42}),
|
|
);
|
|
let authority_result = crate::EventMaterializer::materialize(&materializer, &authority);
|
|
let lockup_result = crate::EventMaterializer::materialize(&materializer, &lockup);
|
|
assert_eq!(authority_result.outputs[0].output_key, "stake_authority:authorize_with_seed:0");
|
|
assert_eq!(
|
|
authority_result.outputs[0].payload_json["projection"]["newAuthority"],
|
|
"new111"
|
|
);
|
|
assert_eq!(lockup_result.outputs[0].output_key, "stake_lockup:set_lockup_checked:0");
|
|
assert_eq!(
|
|
lockup_result.outputs[0].payload_json["projection"]["checkedCustodianFromAccount"],
|
|
true
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn stake_value_projection_does_not_duplicate_lamport_changes() {
|
|
let materializer = crate::StakingMaterializer;
|
|
let observation = observation(
|
|
super::STAKE_SURFACE,
|
|
"withdraw",
|
|
crate::EventFamily::Staking,
|
|
false,
|
|
serde_json::json!([
|
|
account("stake_account", "stake111"),
|
|
account("destination", "dest111"),
|
|
]),
|
|
serde_json::json!({"lamports": 99}),
|
|
);
|
|
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
|
assert_eq!(result.outputs[0].output_key, "stake_value:withdraw:0");
|
|
assert_eq!(result.outputs[0].payload_json["projection"]["lamports"], 99);
|
|
assert_eq!(
|
|
result.outputs[0].payload_json["projection"]["lamportBalanceChangesDuplicated"],
|
|
false
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vote_account_and_authority_events_are_projected() {
|
|
let materializer = crate::StakingMaterializer;
|
|
let initialize = observation(
|
|
super::VOTE_SURFACE,
|
|
"initialize_account_v2",
|
|
crate::EventFamily::Lifecycle,
|
|
false,
|
|
serde_json::json!([
|
|
account("vote_account", "vote111"),
|
|
account("validator_identity", "validator111"),
|
|
]),
|
|
serde_json::json!({
|
|
"authorizedVoter": "voter111",
|
|
"authorizedWithdrawer": "withdrawer111",
|
|
"inflationRewardsCommissionBasisPoints": 500,
|
|
}),
|
|
);
|
|
let authorize = observation(
|
|
super::VOTE_SURFACE,
|
|
"authorize_checked",
|
|
crate::EventFamily::Admin,
|
|
false,
|
|
serde_json::json!([
|
|
account("vote_account", "vote111"),
|
|
account("new_authority", "new111"),
|
|
]),
|
|
serde_json::json!({"authorizationType": {"kind": "voter"}, "newAuthorityFromAccount": true}),
|
|
);
|
|
let initialize_result = crate::EventMaterializer::materialize(&materializer, &initialize);
|
|
let authorize_result = crate::EventMaterializer::materialize(&materializer, &authorize);
|
|
assert_eq!(initialize_result.outputs[0].output_key, "vote_account:initialize_account_v2:0");
|
|
assert_eq!(
|
|
initialize_result.outputs[0].payload_json["projection"]["validatorIdentity"],
|
|
"validator111"
|
|
);
|
|
assert_eq!(authorize_result.outputs[0].output_key, "vote_authority:authorize_checked:0");
|
|
assert_eq!(
|
|
authorize_result.outputs[0].payload_json["projection"]["newAuthority"],
|
|
"new111"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn vote_state_and_reward_outputs_are_distinguished() {
|
|
let materializer = crate::StakingMaterializer;
|
|
let vote_state = observation(
|
|
super::VOTE_SURFACE,
|
|
"tower_sync_switch",
|
|
crate::EventFamily::Staking,
|
|
false,
|
|
serde_json::json!([
|
|
account("vote_account", "vote111"),
|
|
account("vote_authority", "authority111"),
|
|
]),
|
|
serde_json::json!({"root": 10, "hash": "hash111", "switchProofHash": "switch111"}),
|
|
);
|
|
let reward = observation(
|
|
super::VOTE_SURFACE,
|
|
"deposit_delegator_rewards",
|
|
crate::EventFamily::Reward,
|
|
false,
|
|
serde_json::json!([
|
|
account("vote_account", "vote111"),
|
|
account("deposit_source", "source111"),
|
|
]),
|
|
serde_json::json!({"depositLamports": 123}),
|
|
);
|
|
let vote_result = crate::EventMaterializer::materialize(&materializer, &vote_state);
|
|
let reward_result = crate::EventMaterializer::materialize(&materializer, &reward);
|
|
assert_eq!(vote_result.outputs[0].output_key, "vote_state:tower_sync_switch:0");
|
|
assert_eq!(
|
|
vote_result.outputs[0].payload_json["projection"]["switchProofHash"],
|
|
"switch111"
|
|
);
|
|
assert_eq!(reward_result.outputs[0].family, crate::MaterializedEventFamily::Reward);
|
|
assert_eq!(reward_result.outputs[0].output_key, "vote_reward:deposit_delegator_rewards:0");
|
|
}
|
|
|
|
#[test]
|
|
fn failed_staking_mutation_is_refused() {
|
|
let materializer = crate::StakingMaterializer;
|
|
let observation = observation(
|
|
super::STAKE_SURFACE,
|
|
"delegate_stake",
|
|
crate::EventFamily::Staking,
|
|
true,
|
|
serde_json::json!([account("stake_account", "stake111")]),
|
|
serde_json::json!({}),
|
|
);
|
|
let result = crate::EventMaterializer::materialize(&materializer, &observation);
|
|
assert_eq!(result.status, crate::MaterializerOutcomeStatus::Refused);
|
|
assert!(result.outputs.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn unrelated_or_disabled_observations_are_ignored() {
|
|
let materializer = crate::StakingMaterializer;
|
|
let unrelated = observation(
|
|
"solana_native_system",
|
|
"create_account",
|
|
crate::EventFamily::Lifecycle,
|
|
false,
|
|
serde_json::json!([]),
|
|
serde_json::json!({}),
|
|
);
|
|
let redelegate = observation(
|
|
super::STAKE_SURFACE,
|
|
"redelegate",
|
|
crate::EventFamily::Staking,
|
|
false,
|
|
serde_json::json!([]),
|
|
serde_json::json!({"officiallyDisabled": true}),
|
|
);
|
|
let unrelated_result = crate::EventMaterializer::materialize(&materializer, &unrelated);
|
|
let redelegate_result = crate::EventMaterializer::materialize(&materializer, &redelegate);
|
|
assert_eq!(unrelated_result.status, crate::MaterializerOutcomeStatus::Ignored);
|
|
assert_eq!(redelegate_result.status, crate::MaterializerOutcomeStatus::Ignored);
|
|
}
|
|
|
|
#[test]
|
|
fn output_serialization_is_deterministic() {
|
|
let materializer = crate::StakingMaterializer;
|
|
let observation = observation(
|
|
super::VOTE_SURFACE,
|
|
"update_commission_bps",
|
|
crate::EventFamily::Admin,
|
|
false,
|
|
serde_json::json!([account("vote_account", "vote111")]),
|
|
serde_json::json!({"commissionBasisPoints": 250, "commissionKind": "inflation_rewards"}),
|
|
);
|
|
let first = crate::EventMaterializer::materialize(&materializer, &observation);
|
|
let second = crate::EventMaterializer::materialize(&materializer, &observation);
|
|
let first_json_result = serde_json::to_string(&first.outputs[0].payload_json);
|
|
let first_json = match first_json_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("unexpected serialization error: {error}"),
|
|
};
|
|
let second_json_result = serde_json::to_string(&second.outputs[0].payload_json);
|
|
let second_json = match second_json_result {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => panic!("unexpected serialization error: {error}"),
|
|
};
|
|
assert_eq!(first_json, second_json);
|
|
}
|
|
}
|