// file: kb-lib/src/materializer/staking/core.rs // version: 8 //! Instruction-level staking projections for native Stake and Vote observations. const ACCEPTED_FAMILIES: &[crate::MdEventFamily] = &[ crate::MdEventFamily::Lifecycle, crate::MdEventFamily::Admin, crate::MdEventFamily::Staking, crate::MdEventFamily::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::MdMaterializedEventFamily { return match self { Self::VoteReward => crate::MdMaterializedEventFamily::Reward, _ => crate::MdMaterializedEventFamily::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 MtStakingMaterializer; impl crate::MtMaterializer for crate::MtStakingMaterializer { 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::MdDecodedProtocolEvent) -> 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::MdDecodedProtocolEvent, ) -> kb_core::Result> { if !crate::MtMaterializer::accepts_event(self, event) { return std::result::Result::Ok(std::vec::Vec::new()); } return std::result::Result::Ok(std::vec::Vec::new()); } } impl crate::MtApiEventMaterializer for crate::MtStakingMaterializer { fn identity(&self) -> crate::MtApiMaterializerIdentity { return crate::MtApiMaterializerIdentity { name: "solana_native_staking".to_string(), version: env!("CARGO_PKG_VERSION").to_string(), }; } fn accepted_families(&self) -> &'static [crate::MdEventFamily] { return ACCEPTED_FAMILIES; } fn accepts_observation(&self, observation: &crate::DcApiDecodedObservation) -> 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::MdEventFamily, ) -> crate::MtApiMaterializationTransactionPolicy { return crate::MtApiMaterializationTransactionPolicy::SuccessfulCommittedOnly; } fn materialize( &self, observation: &crate::DcApiDecodedObservation, ) -> crate::MtApiMaterializerExecutionResult { 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::TRACING_TARGET_MATERIALIZER_STAKING, 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::MtApiMaterializerExecutionResult::ignored(); }, }; if !family_is_accepted(observation.event.event_family) { return crate::MtApiMaterializerExecutionResult::ignored(); } if observation.transaction_failed || !observation.observation_committed { tracing::debug!( target: crate::TRACING_TARGET_MATERIALIZER_STAKING, 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::MtApiMaterializerExecutionResult::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::MtApiMaterializedOutput { 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::TRACING_TARGET_MATERIALIZER_STAKING, 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::MtApiMaterializerExecutionResult { status: crate::MtApiMaterializerOutcomeStatus::Inserted, outputs: std::vec![output], diagnostics: std::vec::Vec::new(), }; } } fn family_is_accepted(family: crate::MdEventFamily) -> bool { return ACCEPTED_FAMILIES.iter().any(|accepted| return *accepted == family); } fn projection(surface_code: &str, entry_code: &str) -> std::option::Option { 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 { 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 { 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::MdEventFamily, transaction_failed: bool, accounts: serde_json::Value, parameters: serde_json::Value, ) -> crate::DcApiDecodedObservation { return crate::DcApiDecodedObservation { event_key: format!("{entry_code}:0"), event: crate::MdDecodedProtocolEvent { signature: crate::MdSignature("signature".to_string()), slot: crate::MdSlot(42), instruction_path: crate::MdInstructionPath("0".to_string()), program_id: crate::MdProgramId("program-id".to_string()), protocol_code: crate::MdProtocolCode("solana_native".to_string()), surface_code: crate::MdSurfaceCode(surface_code.to_string()), event_code: crate::MdEventCode(format!("{surface_code}.{entry_code}")), event_name: crate::MdEventName(entry_code.to_string()), event_family: family, source_kind: crate::MdEventSourceKind::Instruction, confidence: crate::MdDecoderConfidence::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::DcApiDecoderProof { kind: crate::DcApiDecoderProofKind::Manual, confidence: crate::MdDecoderConfidence::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::MtStakingMaterializer; let observation = observation( super::STAKE_SURFACE, "initialize", crate::MdEventFamily::Lifecycle, false, serde_json::json!([account("stake_account", "stake111")]), serde_json::json!({"authorized": {"staker": "staker111"}}), ); let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Inserted); assert_eq!(result.outputs[0].family, crate::MdMaterializedEventFamily::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::MtStakingMaterializer; let authority = observation( super::STAKE_SURFACE, "authorize_with_seed", crate::MdEventFamily::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::MdEventFamily::Admin, false, serde_json::json!([ account("stake_account", "stake111"), account("lockup_custodian", "custodian111"), ]), serde_json::json!({"epoch": 42}), ); let authority_result = crate::MtApiEventMaterializer::materialize(&materializer, &authority); let lockup_result = crate::MtApiEventMaterializer::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::MtStakingMaterializer; let observation = observation( super::STAKE_SURFACE, "withdraw", crate::MdEventFamily::Staking, false, serde_json::json!([ account("stake_account", "stake111"), account("destination", "dest111"), ]), serde_json::json!({"lamports": 99}), ); let result = crate::MtApiEventMaterializer::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::MtStakingMaterializer; let initialize = observation( super::VOTE_SURFACE, "initialize_account_v2", crate::MdEventFamily::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::MdEventFamily::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::MtApiEventMaterializer::materialize(&materializer, &initialize); let authorize_result = crate::MtApiEventMaterializer::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::MtStakingMaterializer; let vote_state = observation( super::VOTE_SURFACE, "tower_sync_switch", crate::MdEventFamily::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::MdEventFamily::Reward, false, serde_json::json!([ account("vote_account", "vote111"), account("deposit_source", "source111"), ]), serde_json::json!({"depositLamports": 123}), ); let vote_result = crate::MtApiEventMaterializer::materialize(&materializer, &vote_state); let reward_result = crate::MtApiEventMaterializer::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::MdMaterializedEventFamily::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::MtStakingMaterializer; let observation = observation( super::STAKE_SURFACE, "delegate_stake", crate::MdEventFamily::Staking, true, serde_json::json!([account("stake_account", "stake111")]), serde_json::json!({}), ); let result = crate::MtApiEventMaterializer::materialize(&materializer, &observation); assert_eq!(result.status, crate::MtApiMaterializerOutcomeStatus::Refused); assert!(result.outputs.is_empty()); } #[test] fn unrelated_or_disabled_observations_are_ignored() { let materializer = crate::MtStakingMaterializer; let unrelated = observation( "solana_native_system", "create_account", crate::MdEventFamily::Lifecycle, false, serde_json::json!([]), serde_json::json!({}), ); let redelegate = observation( super::STAKE_SURFACE, "redelegate", crate::MdEventFamily::Staking, false, serde_json::json!([]), serde_json::json!({"officiallyDisabled": true}), ); let unrelated_result = crate::MtApiEventMaterializer::materialize(&materializer, &unrelated); let redelegate_result = crate::MtApiEventMaterializer::materialize(&materializer, &redelegate); assert_eq!(unrelated_result.status, crate::MtApiMaterializerOutcomeStatus::Ignored); assert_eq!(redelegate_result.status, crate::MtApiMaterializerOutcomeStatus::Ignored); } #[test] fn output_serialization_is_deterministic() { let materializer = crate::MtStakingMaterializer; let observation = observation( super::VOTE_SURFACE, "update_commission_bps", crate::MdEventFamily::Admin, false, serde_json::json!([account("vote_account", "vote111")]), serde_json::json!({"commissionBasisPoints": 250, "commissionKind": "inflation_rewards"}), ); let first = crate::MtApiEventMaterializer::materialize(&materializer, &observation); let second = crate::MtApiEventMaterializer::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); } }