// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs // version: 23 //! Real Devnet simulation and submission for current Metaplex Token Metadata operations. /// Complete request for one real Devnet Metaplex Token Metadata execution. #[derive(Clone, Debug, PartialEq)] pub struct DevnetMetaplexTokenMetadataExecutionRequest { /// Stable caller-provided execution identifier. pub intent_id: std::string::String, /// Endpoint role used for reads, balance, blockhash and fee queries. pub query_role: std::string::String, /// Endpoint role used for simulation, submission and confirmation. pub transaction_role: std::string::String, /// Exact current Metaplex operation. pub operation: ks_lib::ExMetaplexTokenMetadataOperation, /// Additional non-profile signers explicitly authorized for this exact operation. pub additional_authorized_signers: std::vec::Vec, /// Bounded state reads required before execution. pub preflight_reads: std::vec::Vec, /// Bounded state reads repeated after confirmed submission. pub postcondition_reads: std::vec::Vec, /// Explicitly authorizes signing and submission after successful simulation. pub submit: bool, /// Explicit operator confirmation required for submission. pub operator_confirmed: bool, /// Materializes canonical account snapshots after confirmed submission. pub materialize_after_confirmation: bool, /// Number of `getTransaction` retries after the first hydration attempt. pub post_validation_max_retries: u32, /// Replaces existing core and decode outputs for the submitted signature. pub force_post_validation_replay: bool, } impl crate::DevnetMetaplexTokenMetadataExecutionRequest { /// Creates a conservative simulation-only request. pub fn new( intent_id: impl std::convert::Into, operation: ks_lib::ExMetaplexTokenMetadataOperation, ) -> Self { return Self { intent_id: intent_id.into(), query_role: "http_queries".to_string(), transaction_role: "http_transactions".to_string(), operation, additional_authorized_signers: std::vec::Vec::new(), preflight_reads: std::vec::Vec::new(), postcondition_reads: std::vec::Vec::new(), submit: false, operator_confirmed: false, materialize_after_confirmation: false, post_validation_max_retries: 10, force_post_validation_replay: false, }; } /// Creates a conservative request from one serialized typed Metaplex operation. /// /// This entry point lets automated Devnet campaigns and the desktop reuse the /// complete executor surface without duplicating one Rust constructor per /// operation. The JSON must deserialize to `ExMetaplexTokenMetadataOperation`. pub fn from_operation_json( intent_id: impl std::convert::Into, operation_json: &str, ) -> ks_core::Result { if operation_json.contains("\"operation\":\"...\"") || operation_json.contains("\"operation\": \"...\"") || operation_json.trim() == "..." { return std::result::Result::Err(ks_core::Error::config( "Metaplex operation JSON still contains the documentation placeholder `...`; use metaplex_token_metadata_current_operation_json_template or one of the named request constructors", )); } let operation = match serde_json::from_str::( operation_json, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(ks_core::Error::config(format!( "invalid typed Metaplex operation JSON: {error}" ))); }, }; let request = Self::new(intent_id, operation); if let std::result::Result::Err(error) = request.validate() { return std::result::Result::Err(error); } return std::result::Result::Ok(request); } /// Validates request-local bounds and rejects deprecated operations. pub fn validate(&self) -> ks_core::Result<()> { if self.intent_id.trim().is_empty() { return std::result::Result::Err(ks_core::Error::config( "Devnet Metaplex execution intent id must not be empty", )); } if self.query_role.trim().is_empty() || self.transaction_role.trim().is_empty() { return std::result::Result::Err(ks_core::Error::config( "Devnet Metaplex execution endpoint roles must not be empty", )); } if self.additional_authorized_signers.len() > 4 { return std::result::Result::Err(ks_core::Error::config( "Devnet Metaplex execution supports at most four additional authorized signers", )); } let mut unique_authorized_signers = std::collections::BTreeSet::new(); for signer in &self.additional_authorized_signers { if let std::result::Result::Err(error) = ks_onchain_transport::validate_solana_pubkey_text( signer.0.as_str(), "Metaplex additional authorized signer", ) { return std::result::Result::Err(error); } if !unique_authorized_signers.insert(signer.0.as_str()) { return std::result::Result::Err(ks_core::Error::config( "Devnet Metaplex additional authorized signers must be unique", )); } } if self.preflight_reads.len() > ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS || self.postcondition_reads.len() > ks_pipeline::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS { return std::result::Result::Err(ks_core::Error::config( "Devnet Metaplex execution stateful read limit exceeded", )); } if self.post_validation_max_retries > 20 { return std::result::Result::Err(ks_core::Error::config( "post-execution getTransaction retries must not exceed 20", )); } if self.submit && self.materialize_after_confirmation && self.postcondition_reads.is_empty() { return std::result::Result::Err(ks_core::Error::config( "Metaplex materialization requires at least one postcondition account read", )); } let operation_code = self.operation.operation_code(); if ks_lib::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES.contains(&operation_code) { return std::result::Result::Err(ks_core::Error::new( "execution_metaplex_devnet_deprecated_operation_forbidden", "automatic Devnet campaigns do not execute deprecated Metaplex operations", )); } return std::result::Result::Ok(()); } } /// Returns the current operation names accepted by automatic Devnet campaigns. pub fn metaplex_token_metadata_current_operation_names() -> &'static [&'static str] { return &[ "create", "update_as_update_authority_v2", "verify", "unverify", "burn", "delegate", "revoke", "lock", "unlock", "transfer", "close_accounts", "create_escrow_account", "close_escrow_account", "transfer_out_of_escrow", "mint", "migrate", "use", "collect", "print", "resize", ]; } /// Returns a formatted JSON template for the first named Devnet campaign operations. /// /// Placeholders beginning with `<` must be replaced before parsing or execution. pub fn metaplex_token_metadata_current_operation_json_template( operation: &str, ) -> ks_core::Result { let value = match operation { "create" => serde_json::json!({ "operation": "create", "metadata": "", "master_edition": "", "mint": "", "mint_as_signer": false, "authority": "", "update_authority": "", "update_authority_as_signer": true, "spl_token_program": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA", "create_args": { "V1": { "name": "Khadhroony Devnet Asset", "symbol": "KHDEV", "uri": "https://example.invalid/khadhroony-devnet.json", "seller_fee_basis_points": 0, "creators": null, "primary_sale_happened": false, "is_mutable": true, "token_standard": "NonFungible", "collection": null, "uses": null, "collection_details": null, "rule_set": null, "decimals": null, "print_supply": "Zero" } } }), "update_as_update_authority_v2" => serde_json::json!({ "operation": "update_as_update_authority_v2", "authority": "", "mint": "", "metadata": "", "edition": null, "token": null, "authorization_rules_program": null, "authorization_rules": null, "update_args": { "AsUpdateAuthorityV2": { "new_update_authority": null, "data": null, "primary_sale_happened": null, "is_mutable": null, "collection": null, "collection_details": null, "uses": null, "rule_set": null, "authorization_data": null } } }), "verify_creator" | "verify" => serde_json::json!({ "operation": "verify", "authority": "", "delegate_record": null, "metadata": "", "collection_mint": null, "collection_metadata": null, "collection_master_edition": null, "verification_args": "CreatorV1" }), "unverify_creator" | "unverify" => serde_json::json!({ "operation": "unverify", "authority": "", "delegate_record": null, "metadata": "", "collection_mint": null, "collection_metadata": null, "verification_args": "CreatorV1" }), _ => { return std::result::Result::Err(ks_core::Error::config(format!( "no named JSON template for `{operation}`; current operations: {}", metaplex_token_metadata_current_operation_names().join(", ") ))); }, }; return match serde_json::to_string_pretty(&value) { std::result::Result::Ok(value) => std::result::Result::Ok(value), std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::json(format!( "unable to serialize Metaplex operation template: {error}" ))), }; } /// Creates one named creator verification request without handwritten operation JSON. pub fn devnet_metaplex_creator_verify_request( intent_id: impl std::convert::Into, authority: ks_lib::MdPubkey, metadata: ks_lib::MdPubkey, ) -> crate::DevnetMetaplexTokenMetadataExecutionRequest { return crate::DevnetMetaplexTokenMetadataExecutionRequest::new( intent_id, ks_lib::ExMetaplexTokenMetadataOperation::Verify { authority, delegate_record: std::option::Option::None, metadata, collection_mint: std::option::Option::None, collection_metadata: std::option::Option::None, collection_master_edition: std::option::Option::None, verification_args: mpl_token_metadata::types::VerificationArgs::CreatorV1, }, ); } /// Creates one named creator unverification request without handwritten operation JSON. pub fn devnet_metaplex_creator_unverify_request( intent_id: impl std::convert::Into, authority: ks_lib::MdPubkey, metadata: ks_lib::MdPubkey, ) -> crate::DevnetMetaplexTokenMetadataExecutionRequest { return crate::DevnetMetaplexTokenMetadataExecutionRequest::new( intent_id, ks_lib::ExMetaplexTokenMetadataOperation::Unverify { authority, delegate_record: std::option::Option::None, metadata, collection_mint: std::option::Option::None, collection_metadata: std::option::Option::None, verification_args: mpl_token_metadata::types::VerificationArgs::CreatorV1, }, ); } /// Complete evidence produced by one real Devnet Metaplex execution. #[derive(Clone, Debug, PartialEq)] pub struct DevnetMetaplexTokenMetadataExecutionSummary { /// Profile used by the orchestration. pub profile_name: std::string::String, /// Exact classified cluster. pub cluster: ks_lib::ExApiExecutionCluster, /// Genesis hash returned by the selected endpoint. pub genesis_hash: std::string::String, /// Non-secret persistent wallet description. pub wallet: ks_wallet::WalletSummary, /// Wallet balance observed before planning. pub balance_lamports: u64, /// Stateful snapshots observed before simulation. pub before: std::vec::Vec, /// Stateful preflight report bound to the exact plan. pub stateful_preflight: ks_pipeline::MetaplexTokenMetadataPreflightReport, /// Exact prepared Metaplex plan. pub plan: ks_lib::ExApiPreparedExecutionPlan, /// Recent blockhash used by the exact transaction. pub latest_blockhash: ks_onchain_transport::LatestBlockhashResult, /// Fee estimate for the exact compiled message. pub fee: ks_onchain_transport::FeeForMessageResult, /// Exact real RPC simulation result. pub simulation: ks_lib::ExApiExecutionSimulationResult, /// RPC context slot returned by the exact simulation call. pub simulation_context_slot: u64, /// Readiness report proving exact-message simulation and signer resolution. pub readiness: ks_pipeline::MetaplexTokenMetadataExecutionReadinessReport, /// Submission result when explicitly authorized. pub send_result: std::option::Option, /// Confirmation result when submitted. pub confirmation: std::option::Option, /// Stateful snapshots observed after confirmed submission. pub after: std::vec::Vec, /// Whether canonical account materialization was requested. pub materialization_requested: bool, /// Canonical projections emitted from confirmed account snapshots. pub materialized_snapshots: std::vec::Vec, /// Canonical hydration result for the exact submitted signature. pub backfill: std::option::Option, /// Core extraction result for the exact submitted signature. pub core_extraction: std::option::Option, /// First Metaplex decode and materialization replay. pub decode_replay: std::option::Option, /// Second replay proving idempotence for the exact same input. pub idempotence_replay: std::option::Option, /// Exact materialized rows produced for the submitted Metaplex transaction. pub materializations: std::vec::Vec, /// Aggregated canonical post-execution diagnostic. pub post_execution: std::option::Option, } struct PreparedMetaplexExecution { wallet: ks_wallet::TemporaryWallet, unsigned: ks_lib::ExSolanaUnsignedTransaction, evidence: ks_lib::ExSolanaSimulationEvidence, summary: crate::DevnetMetaplexTokenMetadataExecutionSummary, } /// Simulates one current Metaplex operation against a real Devnet endpoint. pub async fn simulate_devnet_metaplex_token_metadata( http_pool: &ks_onchain_transport::HttpEndpointPool, profile: &ks_config::ProfileConfig, workspace_root: &std::path::Path, request: &crate::DevnetMetaplexTokenMetadataExecutionRequest, observer: &O, ) -> ks_core::Result where O: crate::SolanaExecutionObserver, { if request.submit { return std::result::Result::Err(ks_core::Error::config( "simulate_devnet_metaplex_token_metadata requires submit=false", )); } let prepared = match prepare_execution(http_pool, profile, workspace_root, request, &[], observer).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(prepared.summary); } /// Executes one real Devnet Metaplex simulation or explicitly authorized submission. #[allow(clippy::too_many_arguments)] pub async fn execute_devnet_metaplex_token_metadata( http_pool: &ks_onchain_transport::HttpEndpointPool, store: &S, profile: &ks_config::ProfileConfig, workspace_root: &std::path::Path, request: &crate::DevnetMetaplexTokenMetadataExecutionRequest, decoders: &[std::sync::Arc], materializers: &[std::sync::Arc], observer: &O, ) -> ks_core::Result where S: ks_store::RawTransactionStore + ks_store::CoreExtractionStore + ks_store::DecodePipelineStore + Sync, O: crate::SolanaExecutionObserver, { return crate::execute_devnet_metaplex_token_metadata_with_signers( http_pool, store, profile, workspace_root, request, decoders, materializers, &[], observer, ) .await; } /// Executes one real Devnet Metaplex submission with explicitly authorized extra signers. #[allow(clippy::too_many_arguments)] pub async fn execute_devnet_metaplex_token_metadata_with_signers( http_pool: &ks_onchain_transport::HttpEndpointPool, store: &S, profile: &ks_config::ProfileConfig, workspace_root: &std::path::Path, request: &crate::DevnetMetaplexTokenMetadataExecutionRequest, decoders: &[std::sync::Arc], materializers: &[std::sync::Arc], additional_signers: &[&(dyn solana_signer::Signer + std::marker::Sync)], observer: &O, ) -> ks_core::Result where S: ks_store::RawTransactionStore + ks_store::CoreExtractionStore + ks_store::DecodePipelineStore + Sync, O: crate::SolanaExecutionObserver, { let prepared = match prepare_execution( http_pool, profile, workspace_root, request, additional_signers, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if !request.submit { return std::result::Result::Ok(prepared.summary); } if !prepared.summary.simulation.success { return std::result::Result::Err(ks_core::Error::new( "execution_simulation_failed", crate::simulation_failure_message(&prepared.summary.simulation), )); } let send_evaluation = match ks_lib::ExSafetyChecker .evaluate_send(&prepared.summary.plan, &prepared.summary.simulation) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if send_evaluation.decision == ks_lib::ExSafetyDecision::Deny { return std::result::Result::Err(ks_core::Error::new( "execution_send_denied", crate::violation_message(send_evaluation.violations.as_slice()), )); } let signed = match sign_prepared_transaction( prepared.unsigned, &prepared.evidence, prepared.wallet.as_sync_signer(), additional_signers, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = signed.verify_signatures() { return std::result::Result::Err(error); } let signature = signed.primary_signature().clone(); let mut summary = prepared.summary; let mut diagnostic = ks_lib::ExApiPostExecutionDiagnostic { signature: signature.clone(), canonical_inserted: false, core_extracted: false, decode_replayed: false, materialized: false, diagnostics: std::vec::Vec::new(), }; let send_config = match ks_onchain_transport::SendTransactionConfig::from_execution_config( &profile.execution, std::option::Option::Some(summary.latest_blockhash.context.slot), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let sent = match http_pool .send_transaction_for_role( request.transaction_role.as_str(), signed.transaction_base64().as_str(), &signature, &send_config, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { diagnostic.diagnostics.push(format!("Metaplex submission failed: {error}")); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); }, }; summary.send_result = std::option::Option::Some(sent.to_execution_result(ks_lib::ExApiExecutionCluster::Devnet)); let confirmation_config = match ks_onchain_transport::ConfirmTransactionConfig::from_execution_config( &profile.execution, std::option::Option::Some(summary.latest_blockhash.last_valid_block_height), std::option::Option::Some(summary.latest_blockhash.context.slot), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let confirmation = match http_pool .confirm_transaction_for_roles( request.transaction_role.as_str(), request.query_role.as_str(), ks_lib::ExApiExecutionCluster::Devnet, &signature, &confirmation_config, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { diagnostic.diagnostics.push(format!("Metaplex confirmation failed: {error}")); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); }, }; let confirmation_status = confirmation.status; summary.confirmation = std::option::Option::Some(confirmation); if !matches!( confirmation_status, ks_lib::ExApiExecutionConfirmationStatus::Confirmed | ks_lib::ExApiExecutionConfirmationStatus::Finalized ) { diagnostic.diagnostics.push(format!( "Metaplex post-validation stopped at confirmation status {confirmation_status:?}" )); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); } let postcondition_reads = bind_postcondition_min_context_slot( request.postcondition_reads.as_slice(), summary.confirmation.as_ref().and_then(|value| return value.slot), ); summary.after = match read_snapshots(http_pool, postcondition_reads.as_slice()).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { diagnostic .diagnostics .push(format!("Metaplex stateful postcondition read failed: {error}")); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); }, }; if request.materialize_after_confirmation { summary.materialized_snapshots = materialize_confirmed_snapshots(summary.after.as_slice()); } let backfill = match crate::hydrate_signature( http_pool, store, profile, request.query_role.as_str(), request.post_validation_max_retries, observer, &signature, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { diagnostic.diagnostics.push(format!("Metaplex hydration failed: {error}")); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); }, }; diagnostic.canonical_inserted = crate::canonical_available(&backfill); summary.backfill = std::option::Option::Some(backfill); if !diagnostic.canonical_inserted { diagnostic.diagnostics.push( "confirmed Metaplex transaction was unavailable for canonical hydration".to_string(), ); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); } let extraction = match ks_pipeline::execute_core_extraction( store, &ks_pipeline::CoreExtractionRequest { source: ks_pipeline::CoreExtractionSource::Signatures(std::vec![signature.0.clone()]), limit: 1, max_concurrent_extractions: 1, force_replay: request.force_post_validation_replay, }, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { diagnostic.diagnostics.push(format!("Metaplex core extraction failed: {error}")); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); }, }; diagnostic.core_extracted = extraction.failed == 0 && !extraction.cancelled && extraction.selected == 1 && extraction.extracted.saturating_add(extraction.skipped) >= 1; summary.core_extraction = std::option::Option::Some(extraction); if !diagnostic.core_extracted { diagnostic .diagnostics .push("Metaplex core extraction did not complete".to_string()); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); } let first_replay = match crate::replay_program( store, &signature, false, request.force_post_validation_replay, &[ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID], decoders, materializers, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { diagnostic.diagnostics.push(format!("Metaplex decode replay failed: {error}")); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); }, }; diagnostic.decode_replayed = crate::decode_completed(&first_replay); if !diagnostic.decode_replayed { diagnostic .diagnostics .push(replay_summary_diagnostic("first Metaplex replay", &first_replay)); } summary.decode_replay = std::option::Option::Some(first_replay); let filter = match ks_store::MaterializedEventFilter::new( std::option::Option::None, std::option::Option::None, std::option::Option::Some(signature.0.clone()), 64, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let rows = match ks_store::DecodePipelineStore::list_materialized_events(store, &filter).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { diagnostic .diagnostics .push(format!("Metaplex materialization query failed: {error}")); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); }, }; summary.materializations = metaplex_instruction_materializations(rows); let metaplex_materializer_name = ks_lib::MtApiEventMaterializer::identity( &ks_lib::MtMetadataMetaplexTokenMetadataMaterializer, ) .name; diagnostic.materialized = !summary.plan.policy.post_execution_validation.materialization_required || !summary.materializations.is_empty(); if summary.plan.policy.post_execution_validation.materialization_required && summary.materializations.is_empty() { diagnostic.diagnostics.push(format!( "Metaplex instruction materializer `{metaplex_materializer_name}` produced no queryable row for signature {}", signature.0 )); } let second_replay = match crate::replay_program( store, &signature, true, request.force_post_validation_replay, &[ks_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID], decoders, materializers, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { diagnostic .diagnostics .push(format!("Metaplex idempotence replay failed: {error}")); summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); }, }; let idempotent = second_replay.failed_inputs == 0 && second_replay.processing_error_inputs == 0 && second_replay.processors.iter().all(|processor| { return processor.failed == 0 && processor.processing_errors == 0 && processor.materialized_outputs == 0 && processor.materialization_refused == 0; }); summary.idempotence_replay = std::option::Option::Some(second_replay); if !idempotent { diagnostic .diagnostics .push("second Metaplex replay did not prove a clean idempotent skip".to_string()); } else if diagnostic.canonical_inserted && diagnostic.core_extracted && diagnostic.decode_replayed && diagnostic.materialized { diagnostic.diagnostics.push( "Metaplex completed canonical hydration, core extraction, decode, materialization and idempotence validation" .to_string(), ); } summary.post_execution = std::option::Option::Some(diagnostic); return std::result::Result::Ok(summary); } fn sign_prepared_transaction( unsigned: ks_lib::ExSolanaUnsignedTransaction, evidence: &ks_lib::ExSolanaSimulationEvidence, wallet: &(dyn solana_signer::Signer + std::marker::Sync), additional_signers: &[&(dyn solana_signer::Signer + std::marker::Sync)], ) -> ks_core::Result { let mut signers: std::vec::Vec<&dyn solana_signer::Signer> = std::vec![wallet]; for signer in additional_signers { let signer: &dyn solana_signer::Signer = *signer; signers.push(signer); } return unsigned.sign_after_simulation(evidence, signers.as_slice()); } fn replay_summary_diagnostic( label: &str, summary: &ks_pipeline::DecodeReplaySummary, ) -> std::string::String { let processors = summary .processors .iter() .map(|processor| { return format!( "{}@{} dispatched={} skipped={} decoded={} ignored={} unsupported={} failed={} processing_errors={} materialized_outputs={} materialization_refused={}", processor.processor_name, processor.processor_version, processor.dispatched, processor.skipped, processor.decoded, processor.ignored, processor.unsupported, processor.failed, processor.processing_errors, processor.materialized_outputs, processor.materialization_refused, ); }) .collect::>() .join("; "); return format!( "{label}: selected={} started={} completed={} unmatched={} failed_inputs={} processing_error_inputs={} cancelled={} processors=[{}]", summary.selected, summary.started, summary.completed, summary.unmatched, summary.failed_inputs, summary.processing_error_inputs, summary.cancelled, processors, ); } async fn prepare_execution( http_pool: &ks_onchain_transport::HttpEndpointPool, profile: &ks_config::ProfileConfig, workspace_root: &std::path::Path, request: &crate::DevnetMetaplexTokenMetadataExecutionRequest, additional_signers: &[&(dyn solana_signer::Signer + std::marker::Sync)], observer: &O, ) -> ks_core::Result where O: crate::SolanaExecutionObserver, { if let std::result::Result::Err(error) = request.validate() { return std::result::Result::Err(error); } if let std::result::Result::Err(error) = validate_profile(profile, request) { return std::result::Result::Err(error); } if let std::result::Result::Err(error) = crate::ensure_not_cancelled(observer, "metaplex_validate") { return std::result::Result::Err(error); } let genesis = match http_pool.get_genesis_hash_for_role(request.query_role.as_str()).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if genesis.classified_cluster != std::option::Option::Some(ks_lib::ExApiExecutionCluster::Devnet) { return std::result::Result::Err(ks_core::Error::new( "execution_cluster_mismatch", format!( "expected Devnet genesis hash but endpoint returned {} classified as {:?}", genesis.genesis_hash, genesis.classified_cluster ), )); } let wallet = match crate::load_profile_temporary_wallet(profile, workspace_root).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let wallet_summary = wallet.summary(); let fee_payer = ks_lib::MdPubkey(wallet_summary.public_key.clone()); let balance = match http_pool .get_balance_for_role( request.query_role.as_str(), &fee_payer, &ks_onchain_transport::GetBalanceConfig::confirmed(), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if balance.lamports < profile.execution.max_fee_lamports { return std::result::Result::Err(ks_core::Error::new( "execution_balance_insufficient", "Devnet wallet balance is below the configured fee ceiling", )); } let plan = match build_plan(profile, request, fee_payer.clone()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let before = match read_snapshots(http_pool, &request.preflight_reads).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let stateful_preflight = match ks_pipeline::inspect_metaplex_token_metadata_preflight( &ks_pipeline::MetaplexTokenMetadataPreflightRequest { plan: plan.clone(), snapshots: before.clone(), allow_deprecated_operation: false, }, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let plan_evaluation = match ks_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if plan_evaluation.decision == ks_lib::ExSafetyDecision::Deny { return std::result::Result::Err(ks_core::Error::new( "execution_plan_denied", crate::violation_message(plan_evaluation.violations.as_slice()), )); } let latest_blockhash = match http_pool .get_latest_blockhash_for_role( request.query_role.as_str(), &ks_onchain_transport::GetLatestBlockhashConfig::confirmed(), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let unsigned = match ks_lib::executor_solana_build_legacy_transaction( &plan, latest_blockhash.blockhash.as_str(), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let resolved_signers = match validate_available_signers( unsigned.required_signer_pubkeys(), wallet_summary.public_key.as_str(), request.additional_authorized_signers.as_slice(), additional_signers, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let fee = match http_pool .get_fee_for_message_for_role( request.query_role.as_str(), unsigned.message_base64().as_str(), &ks_onchain_transport::GetFeeForMessageConfig::new( ks_onchain_transport::RpcCommitmentLevel::Confirmed, std::option::Option::Some(latest_blockhash.context.slot), ), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let unsigned_base64 = match unsigned.transaction_base64() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let simulation_config = match ks_onchain_transport::SimulateTransactionConfig::new( ks_onchain_transport::RpcCommitmentLevel::Confirmed, false, false, std::option::Option::Some(latest_blockhash.context.slot), true, std::option::Option::None, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; crate::emit( observer, crate::SolanaExecutionProgressLevel::Info, "metaplex_token_metadata_simulation", format!("simulating exact Metaplex message {}", unsigned.message_hash()), std::option::Option::None, ); let simulation_rpc = match http_pool .simulate_transaction_for_role( request.transaction_role.as_str(), unsigned_base64.as_str(), &simulation_config, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let simulation = simulation_rpc.to_execution_result( ks_lib::ExApiExecutionCluster::Devnet, ks_lib::ExApiExecutionBlockhashKind::Latest, std::option::Option::Some( simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot), ), std::option::Option::None, std::option::Option::None, std::option::Option::Some(&fee), ); let readiness = match ks_pipeline::validate_metaplex_token_metadata_execution_readiness( &ks_pipeline::MetaplexTokenMetadataExecutionReadinessRequest { plan: plan.clone(), preflight: stateful_preflight.clone(), message_hash: unsigned.message_hash().to_string(), simulated_message_hash: unsigned.message_hash().to_string(), simulated: true, simulation_succeeded: simulation.success, resolved_signers, submit: request.submit, operator_confirmed: request.operator_confirmed, }, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { let simulation_json = match serde_json::to_string_pretty(&simulation) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => "".to_string(), }; return std::result::Result::Err(ks_core::Error::new( error.code(), format!("{}; simulation={simulation_json}", error.message()), )); }, }; let evidence = unsigned.bind_simulation(simulation.clone()); return std::result::Result::Ok(PreparedMetaplexExecution { wallet, unsigned, evidence, summary: crate::DevnetMetaplexTokenMetadataExecutionSummary { profile_name: profile.name.clone(), cluster: ks_lib::ExApiExecutionCluster::Devnet, genesis_hash: genesis.genesis_hash, wallet: wallet_summary, balance_lamports: balance.lamports, before, stateful_preflight, plan, latest_blockhash, fee, simulation, simulation_context_slot: simulation_rpc.context.slot, readiness, send_result: std::option::Option::None, confirmation: std::option::Option::None, after: std::vec::Vec::new(), materialization_requested: request.materialize_after_confirmation, materialized_snapshots: std::vec::Vec::new(), backfill: std::option::Option::None, core_extraction: std::option::Option::None, decode_replay: std::option::Option::None, idempotence_replay: std::option::Option::None, materializations: std::vec::Vec::new(), post_execution: std::option::Option::None, }, }); } fn metaplex_instruction_materializations( rows: std::vec::Vec, ) -> std::vec::Vec { let materializer_name = ks_lib::MtApiEventMaterializer::identity( &ks_lib::MtMetadataMetaplexTokenMetadataMaterializer, ) .name; return rows .into_iter() .filter(|row| return row.processor_name == materializer_name) .collect(); } fn materialize_confirmed_snapshots( snapshots: &[ks_pipeline::MetaplexTokenMetadataStatefulReadResult], ) -> std::vec::Vec { return snapshots .iter() .map(|result| { return serde_json::json!({ "projectionVersion": 1, "domain": "metaplex_token_metadata", "projectionSemantics": "confirmed_bounded_account_snapshot", "account": &result.snapshot.account, "accountKind": &result.snapshot.account_kind, "mint": &result.snapshot.mint, "slot": result.snapshot.slot.to_string(), "commitment": &result.commitment, "state": &result.snapshot.payload_json, "externalUriFetched": false }); }) .collect(); } fn bind_postcondition_min_context_slot( requests: &[ks_pipeline::MetaplexTokenMetadataStatefulReadRequest], confirmation_slot: std::option::Option, ) -> std::vec::Vec { return requests .iter() .cloned() .map(|mut request| { request.min_context_slot = match (request.min_context_slot, confirmation_slot) { (std::option::Option::Some(existing), std::option::Option::Some(confirmed)) => { std::option::Option::Some(existing.max(confirmed)) }, (std::option::Option::None, std::option::Option::Some(confirmed)) => { std::option::Option::Some(confirmed) }, (existing, std::option::Option::None) => existing, }; return request; }) .collect(); } async fn read_snapshots( http_pool: &ks_onchain_transport::HttpEndpointPool, requests: &[ks_pipeline::MetaplexTokenMetadataStatefulReadRequest], ) -> ks_core::Result> { let mut results = std::vec::Vec::with_capacity(requests.len()); for request in requests { let value = match ks_pipeline::read_metaplex_token_metadata_stateful_snapshot(http_pool, request) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; results.push(value); } return std::result::Result::Ok(results); } fn build_plan( profile: &ks_config::ProfileConfig, request: &crate::DevnetMetaplexTokenMetadataExecutionRequest, fee_payer: ks_lib::MdPubkey, ) -> ks_core::Result { let intent = ks_lib::ExMetaplexTokenMetadataExecutionIntent { intent_id: request.intent_id.clone(), fee_payer: fee_payer.clone(), max_rent_lamports: profile.execution.devnet_max_spend_lamports, policy: ks_lib::ExApiExecutionPolicy { cluster: ks_lib::ExApiExecutionClusterPolicy { expected_cluster: ks_lib::ExApiExecutionCluster::Devnet, allow_mainnet: false, mainnet_confirmation: false, }, simulation: ks_lib::ExApiExecutionSimulationPolicy::Required, blockhash: ks_lib::ExApiExecutionBlockhashPolicy { kind: ks_lib::ExApiExecutionBlockhashKind::Latest, max_age_slots: std::option::Option::Some( profile.execution.recent_blockhash_max_age_slots, ), nonce_account: std::option::Option::None, nonce_authority: std::option::Option::None, }, cost_limit: ks_lib::ExApiExecutionCostLimit { max_spend_lamports: std::option::Option::Some( profile.execution.devnet_max_spend_lamports, ), max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports), max_compute_unit_price_micro_lamports: std::option::Option::Some( profile.execution.max_compute_unit_price_micro_lamports, ), }, authorized_signers: { let mut authorized_signers = vec![fee_payer.clone()]; for signer in &request.additional_authorized_signers { if !authorized_signers.contains(signer) { authorized_signers.push(signer.clone()); } } authorized_signers }, dry_run: !request.submit, post_execution_validation: ks_lib::ExApiPostExecutionValidationPolicy { canonical_insert_required: request.submit, core_extraction_required: request.submit, decode_replay_required: request.submit, materialization_required: request.submit && request.materialize_after_confirmation, }, }, allow_deprecated_operation: false, operation: request.operation.clone(), }; return ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan( &ks_lib::ExMetadataMetaplexTokenMetadataExecutor, &intent, ); } fn validate_profile( profile: &ks_config::ProfileConfig, request: &crate::DevnetMetaplexTokenMetadataExecutionRequest, ) -> ks_core::Result<()> { if profile.wallet.cluster != "devnet" { return std::result::Result::Err(ks_core::Error::config( "Metaplex Devnet orchestration requires a Devnet wallet profile", )); } if !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist { return std::result::Result::Err(ks_core::Error::config( "Metaplex Devnet orchestration requires an enabled persistent temporary wallet", )); } if !profile.execution.require_simulation { return std::result::Result::Err(ks_core::Error::config( "Metaplex Devnet orchestration requires simulation", )); } if request.submit && !profile.execution.devnet_send_enabled { return std::result::Result::Err(ks_core::Error::config( "Devnet transaction submission is disabled by the wallet profile", )); } if request.submit && profile.execution.require_operator_confirmation && !request.operator_confirmed { return std::result::Result::Err(ks_core::Error::config( "Metaplex Devnet submission requires explicit operator confirmation", )); } return std::result::Result::Ok(()); } fn validate_available_signers( required_signers: &[std::string::String], wallet_pubkey: &str, authorized_additional_signers: &[ks_lib::MdPubkey], additional_signers: &[&(dyn solana_signer::Signer + std::marker::Sync)], ) -> ks_core::Result> { let authorized = authorized_additional_signers .iter() .map(|value| return value.0.as_str()) .collect::>(); let mut available = std::collections::BTreeSet::from([wallet_pubkey.to_string()]); let mut resolved = std::vec![ks_lib::MdPubkey(wallet_pubkey.to_string())]; for signer in additional_signers { let pubkey = signer.pubkey().to_string(); if !authorized.contains(pubkey.as_str()) { return std::result::Result::Err(ks_core::Error::new( "execution_metaplex_undeclared_signer", format!( "Metaplex signer {pubkey} was supplied without explicit request authorization" ), )); } if !available.insert(pubkey.clone()) { return std::result::Result::Err(ks_core::Error::new( "execution_metaplex_duplicate_signer", format!("Metaplex signer {pubkey} was supplied more than once"), )); } resolved.push(ks_lib::MdPubkey(pubkey)); } let missing_authorized = authorized_additional_signers .iter() .filter(|value| return !available.contains(value.0.as_str())) .map(|value| return value.0.clone()) .collect::>(); if !missing_authorized.is_empty() { return std::result::Result::Err(ks_core::Error::new( "execution_metaplex_authorized_signer_unavailable", format!( "explicitly authorized Metaplex signers were not supplied: {}", missing_authorized.join(",") ), )); } let missing = required_signers .iter() .filter(|value| return !available.contains(value.as_str())) .cloned() .collect::>(); if !missing.is_empty() { return std::result::Result::Err(ks_core::Error::new( "execution_metaplex_external_signer_unavailable", format!( "the Devnet Metaplex orchestrator is missing required signers {}; available signers are {}", missing.join(","), available.into_iter().collect::>().join(",") ), )); } return std::result::Result::Ok(resolved); } #[cfg(test)] mod tests { fn example_devnet_profile() -> ks_config::ProfileConfig { let config = match ks_config::parse_config_json(include_str!( "../../../../test-fixtures/config/resolved.app.config.json" )) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("example config parse failed: {error}"), }; return match crate::resolve_demo_devnet_profile(&config, std::option::Option::None) { std::result::Result::Ok(profile) => profile, std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"), }; } fn optional_stateful_reads_from_env( variable: &str, ) -> std::vec::Vec { let value = match std::env::var(variable) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::vec::Vec::new(), }; return match serde_json::from_str::< std::vec::Vec, >(value.as_str()) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("invalid {variable}: {error}"), }; } #[test] #[allow(deprecated)] fn request_rejects_deprecated_operations_and_empty_ids() { let deprecated = crate::DevnetMetaplexTokenMetadataExecutionRequest::new( "deprecated", ks_lib::ExMetaplexTokenMetadataOperation::PuffMetadata { metadata: ks_lib::MdPubkey("11111111111111111111111111111111".to_string()), }, ); assert!(deprecated.validate().is_err()); let mut empty = deprecated; empty.intent_id.clear(); assert!(empty.validate().is_err()); let mut excessive_retries = crate::DevnetMetaplexTokenMetadataExecutionRequest::new( "excessive-retries", ks_lib::ExMetaplexTokenMetadataOperation::Collect { authority: ks_lib::MdPubkey("11111111111111111111111111111111".to_string()), recipient: ks_lib::MdPubkey( "SysvarC1ock11111111111111111111111111111111".to_string(), ), }, ); excessive_retries.post_validation_max_retries = 21; assert!(excessive_retries.validate().is_err()); } #[test] fn typed_operation_json_rejects_invalid_and_deprecated_payloads() { assert!( crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json( "invalid-json", "{}", ) .is_err() ); let deprecated = serde_json::json!({ "operation": "puff_metadata", "metadata": "11111111111111111111111111111111" }); assert!( crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json( "deprecated-operation", deprecated.to_string().as_str(), ) .is_err() ); } #[test] fn named_templates_are_json_and_placeholder_is_rejected_explicitly() { for operation in ["create", "update_as_update_authority_v2", "verify", "unverify"] { let template = super::metaplex_token_metadata_current_operation_json_template(operation); assert!(template.is_ok()); let template = match template { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; let parsed = serde_json::from_str::(template.as_str()); assert!(parsed.is_ok()); if operation == "create" { let parsed = match parsed { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!( parsed .pointer("/create_args/V1/print_supply") .and_then(serde_json::Value::as_str), std::option::Option::Some("Zero"), ); assert_eq!( parsed.get("spl_token_program").and_then(serde_json::Value::as_str), std::option::Option::Some("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",), ); } } let placeholder = crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json( "placeholder", r#"{"operation":"..."}"#, ); assert_eq!( placeholder.as_ref().err().map(ks_core::Error::code), std::option::Option::Some("config"), ); } #[test] fn named_creator_requests_are_conservative_and_current() { let authority = ks_lib::MdPubkey("11111111111111111111111111111111".to_string()); let metadata = ks_lib::MdPubkey("SysvarC1ock11111111111111111111111111111111".to_string()); let verify = super::devnet_metaplex_creator_verify_request( "verify-creator", authority.clone(), metadata.clone(), ); let unverify = super::devnet_metaplex_creator_unverify_request( "unverify-creator", authority, metadata, ); assert!(!verify.submit); assert!(!verify.operator_confirmed); assert!(verify.validate().is_ok()); assert!(unverify.validate().is_ok()); } #[tokio::test] async fn optional_devnet_current_operation_from_env() { if std::env::var("KS_DEVNET_METAPLEX_EXECUTION_TEST").ok().as_deref() != std::option::Option::Some("1") { return; } let operation_json = match std::env::var("KS_DEVNET_METAPLEX_OPERATION_JSON") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { panic!("KS_DEVNET_METAPLEX_OPERATION_JSON is required: {error}"); }, }; let mut profile = example_devnet_profile(); if let std::result::Result::Ok(directory) = std::env::var("KS_DEVNET_WALLET_DIR") { profile.wallet.wallet_dir = directory; } let pool = match ks_onchain_transport::HttpEndpointPool::from_profile(&profile) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"), }; let database_url = match std::env::var("KS_SECRET_POSTGRES_TEST_URL") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { panic!("KS_SECRET_POSTGRES_TEST_URL is required for Metaplex execution: {error}") }, }; let store_options = match ks_store::PostgresStoreOptions::new(database_url, 5, 5_000, false) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("Postgres options failed: {error}"), }; let store = match ks_store::PostgresStore::connect(store_options).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("Postgres connection failed: {error}"), }; if let std::result::Result::Err(error) = store.initialize_store_schema().await { panic!("Postgres schema initialization failed: {error}"); } let workspace_root = match std::path::Path::new(env!("CARGO_MANIFEST_DIR")).parent() { std::option::Option::Some(value) => value, std::option::Option::None => panic!("workspace root cannot be resolved"), }; let mut request = match crate::DevnetMetaplexTokenMetadataExecutionRequest::from_operation_json( format!("devnet-metaplex-test-{}", uuid::Uuid::new_v4()), operation_json.as_str(), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { panic!("Metaplex request parsing failed: {error}") }, }; request.preflight_reads = optional_stateful_reads_from_env("KS_DEVNET_METAPLEX_PREFLIGHT_READS_JSON"); request.postcondition_reads = optional_stateful_reads_from_env("KS_DEVNET_METAPLEX_POSTCONDITION_READS_JSON"); request.submit = std::env::var("KS_DEVNET_METAPLEX_SUBMIT").ok().as_deref() == std::option::Option::Some("1"); request.operator_confirmed = request.submit && std::env::var("KS_DEVNET_METAPLEX_OPERATOR_CONFIRMED").ok().as_deref() == std::option::Option::Some("1"); request.materialize_after_confirmation = request.submit && std::env::var("KS_DEVNET_METAPLEX_MATERIALIZE_AFTER_CONFIRMATION") .ok() .as_deref() == std::option::Option::Some("1"); request.post_validation_max_retries = 20; let decoders: std::vec::Vec> = std::vec![std::sync::Arc::new(ks_lib::DcMetadataMetaplexTokenMetadataDecoder)]; let materializers: std::vec::Vec> = std::vec![ std::sync::Arc::new(ks_lib::MtAdminMaterializer), std::sync::Arc::new(ks_lib::MtComplianceAuditMaterializer), std::sync::Arc::new(ks_lib::MtLifecycleMaterializer), std::sync::Arc::new(ks_lib::MtMetadataMetaplexTokenMetadataMaterializer), std::sync::Arc::new(ks_lib::MtRiskMaterializer), ]; let summary = match crate::execute_devnet_metaplex_token_metadata( &pool, &store, &profile, workspace_root, &request, decoders.as_slice(), materializers.as_slice(), &crate::NoopSolanaExecutionObserver, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("Devnet Metaplex execution failed: {error}"), }; assert!(summary.simulation.success, "{:#?}", summary.simulation); if request.submit { assert!(summary.send_result.is_some()); assert!(summary.confirmation.is_some()); let diagnostic = summary.post_execution.as_ref(); assert!(diagnostic.is_some()); assert!(diagnostic.is_some_and(|value| return value.canonical_inserted && value.core_extracted && value.decode_replayed && value.materialized)); if request.materialize_after_confirmation { assert!(!summary.materializations.is_empty()); } assert!(summary.idempotence_replay.is_some()); } } #[test] fn postcondition_reads_are_bound_to_the_confirmation_slot() { let request = ks_pipeline::MetaplexTokenMetadataStatefulReadRequest { query_role: "rpc".to_string(), account: ks_lib::MdPubkey("11111111111111111111111111111111".to_string()), kind: ks_pipeline::MetaplexTokenMetadataAccountKind::Metadata, min_context_slot: std::option::Option::Some(7), max_data_bytes: 128, }; let bound = super::bind_postcondition_min_context_slot( std::slice::from_ref(&request), std::option::Option::Some(11), ); assert_eq!(bound[0].min_context_slot, std::option::Option::Some(11)); let preserved = super::bind_postcondition_min_context_slot( std::slice::from_ref(&request), std::option::Option::Some(5), ); assert_eq!(preserved[0].min_context_slot, std::option::Option::Some(7)); } #[test] fn materialization_evidence_is_owned_by_the_metaplex_materializer() { fn row(processor_name: &str) -> ks_store::MaterializedEventQueryRow { return ks_store::MaterializedEventQueryRow { processor_name: processor_name.to_string(), processor_version: "0.4.8-pre.13".to_string(), input_key: "input".to_string(), output_key: "output".to_string(), source_event_key: "event".to_string(), source_decoder_name: "metadata.metaplex_token_metadata".to_string(), source_decoder_version: "0.4.8-pre.13".to_string(), signature: "signature".to_string(), slot: 42, materialized_family: "metadata".to_string(), payload_json: serde_json::json!({}), created_at: "created".to_string(), updated_at: "updated".to_string(), }; } let metaplex_name = ks_lib::MtApiEventMaterializer::identity( &ks_lib::MtMetadataMetaplexTokenMetadataMaterializer, ) .name; let rows = super::metaplex_instruction_materializations(vec![ row(metaplex_name.as_str()), row("materializer.metadata.other"), ]); assert_eq!(rows.len(), 1); assert_eq!(rows[0].processor_name, metaplex_name); } #[test] fn signer_guard_requires_every_external_signer_explicitly() { let external = solana_keypair::Keypair::new(); let external_pubkey = solana_signer::Signer::pubkey(&external).to_string(); assert!( super::validate_available_signers(&["wallet".to_string()], "wallet", &[], &[]).is_ok() ); assert!( super::validate_available_signers( std::slice::from_ref(&external_pubkey), "wallet", &[], &[], ) .is_err() ); let authorized = vec![ks_lib::MdPubkey(external_pubkey.clone())]; let resolved = match super::validate_available_signers( &["wallet".to_string(), external_pubkey.clone()], "wallet", authorized.as_slice(), &[&external], ) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return, }; assert_eq!(resolved.len(), 2); assert!(super::validate_available_signers( &["wallet".to_string()], "wallet", &[], &[&external], ) .is_err()); assert!( super::validate_available_signers( &["wallet".to_string(), external_pubkey.clone()], "wallet", authorized.as_slice(), &[], ) .is_err() ); assert!( super::validate_available_signers( &["wallet".to_string(), external_pubkey], "wallet", authorized.as_slice(), &[&external, &external], ) .is_err() ); } }