diff --git a/kb-pipeline/src/lib.rs b/kb-pipeline/src/lib.rs index 4fb0c24..21b6254 100644 --- a/kb-pipeline/src/lib.rs +++ b/kb-pipeline/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-pipeline/src/lib.rs -// version: 14 +// version: 15 #![forbid(unsafe_code)] #![deny(unreachable_pub)] @@ -20,6 +20,7 @@ mod solana_memo_execution; mod solana_stateful; mod solana_token2022_correlation; mod solana_token2022_crypto_preflight; +mod solana_token2022_devnet_execution; mod solana_token2022_devnet_scenarios; mod solana_token2022_execution_orchestration; mod solana_token2022_preflight; @@ -243,6 +244,14 @@ pub use self::solana_token2022_crypto_preflight::Token2022ProofContextRequiremen pub use self::solana_token2022_crypto_preflight::ZK_PROOF_CONTEXT_META_BYTES; /// Inspects bounded Token-2022 cryptographic proof contexts. pub use self::solana_token2022_crypto_preflight::inspect_token2022_cryptographic_preflight; +/// Complete request for one Devnet Token-2022 execution. +pub use self::solana_token2022_devnet_execution::DevnetSplToken2022ExecutionRequest; +/// Complete result of one Devnet Token-2022 execution. +pub use self::solana_token2022_devnet_execution::DevnetSplToken2022ExecutionSummary; +/// Executes one Devnet Token-2022 simulation or authorized submission. +pub use self::solana_token2022_devnet_execution::execute_devnet_spl_token2022; +/// Simulates one Devnet Token-2022 operation after stateful preflight. +pub use self::solana_token2022_devnet_execution::simulate_devnet_spl_token2022; /// Stable category of one independent Devnet validation scenario. pub use self::solana_token2022_devnet_scenarios::DevnetSplValidationFamily; /// Current implementation status of one Devnet validation scenario. diff --git a/kb-pipeline/src/solana_token2022_devnet_execution.rs b/kb-pipeline/src/solana_token2022_devnet_execution.rs new file mode 100644 index 0000000..1ff6787 --- /dev/null +++ b/kb-pipeline/src/solana_token2022_devnet_execution.rs @@ -0,0 +1,921 @@ +// file: kb-pipeline/src/solana_token2022_devnet_execution.rs +// version: 1 + +//! Devnet Token-2022 execution with stateful and canonical post-validation. + +/// Complete request for one Devnet Token-2022 execution. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct DevnetSplToken2022ExecutionRequest { + /// Stable caller-provided execution identifier. + pub intent_id: std::string::String, + /// Endpoint role used for state, balance, blockhash, fee and hydration calls. + pub query_role: std::string::String, + /// Endpoint role used for simulation, submission and confirmation polling. + pub transaction_role: std::string::String, + /// Exact typed Token-2022 operation. + pub operation: kb_lib::ExSplToken2022Operation, + /// Explicitly authorizes signing and submission after successful simulation. + pub submit: bool, + /// Explicit operator confirmation required by the active profile. + pub operator_confirmed: 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::DevnetSplToken2022ExecutionRequest { + /// Creates a conservative simulation-only request. + pub fn new( + intent_id: impl std::convert::Into, + operation: kb_lib::ExSplToken2022Operation, + ) -> Self { + return Self { + intent_id: intent_id.into(), + query_role: "http_queries".to_string(), + transaction_role: "http_transactions".to_string(), + operation, + submit: false, + operator_confirmed: false, + post_validation_max_retries: 10, + force_post_validation_replay: false, + }; + } + + /// Validates request-local bounds independently from one profile. + pub fn validate(&self) -> kb_core::Result<()> { + if self.intent_id.trim().is_empty() { + return std::result::Result::Err(kb_core::Error::config( + "Devnet Token-2022 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(kb_core::Error::config( + "Devnet Token-2022 execution endpoint roles must not be empty", + )); + } + if self.post_validation_max_retries > 20 { + return std::result::Result::Err(kb_core::Error::config( + "post-execution getTransaction retries must not exceed 20", + )); + } + return std::result::Result::Ok(()); + } +} + +/// Complete result of one Devnet Token-2022 execution. +#[derive(Clone, Debug, PartialEq)] +pub struct DevnetSplToken2022ExecutionSummary { + /// Profile used by the orchestration. + pub profile_name: std::string::String, + /// Exact classified cluster. + pub cluster: kb_lib::ExApiExecutionCluster, + /// Genesis hash returned by the selected endpoint. + pub genesis_hash: std::string::String, + /// Non-secret persistent wallet description. + pub wallet: kb_wallet::WalletSummary, + /// Wallet balance observed before planning. + pub balance_lamports: u64, + /// Stateful readiness report produced before plan simulation. + pub stateful_preflight: crate::Token2022PreflightReport, + /// Exact prepared Token plan. + pub plan: kb_lib::ExApiPreparedExecutionPlan, + /// Recent blockhash used by the exact transaction. + pub latest_blockhash: kb_onchain_transport::LatestBlockhashResult, + /// Fee estimate for the exact compiled message. + pub fee: kb_onchain_transport::FeeForMessageResult, + /// Exact simulation result bound to the compiled message. + pub simulation: kb_lib::ExApiExecutionSimulationResult, + /// Submission result when explicitly authorized. + pub send_result: std::option::Option, + /// Confirmation result when submitted. + pub confirmation: std::option::Option, + /// Canonical hydration result for the exact signature. + pub backfill: std::option::Option, + /// Core extraction result for the exact signature. + pub core_extraction: std::option::Option, + /// First Token decode and materialization replay. + pub decode_replay: std::option::Option, + /// Second replay proving idempotence for the same decoder version and input. + pub idempotence_replay: std::option::Option, + /// Exact materialized rows produced for the submitted Token transaction. + pub materializations: std::vec::Vec, + /// Aggregated post-execution validation diagnostic. + pub post_execution: std::option::Option, +} + +struct PreparedToken2022Execution { + wallet: kb_wallet::TemporaryWallet, + unsigned: kb_lib::ExSolanaUnsignedTransaction, + evidence: kb_lib::ExSolanaSimulationEvidence, + summary: crate::DevnetSplToken2022ExecutionSummary, +} + +/// Simulates one Devnet Token-2022 operation after stateful preflight. +pub async fn simulate_devnet_spl_token2022( + http_pool: &kb_onchain_transport::HttpEndpointPool, + profile: &kb_config::ProfileConfig, + workspace_root: &std::path::Path, + request: &crate::DevnetSplToken2022ExecutionRequest, + observer: &O, +) -> kb_core::Result +where + O: crate::SolanaExecutionObserver, +{ + if request.submit { + return std::result::Result::Err(kb_core::Error::config( + "simulate_devnet_spl_token2022 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 Devnet Token-2022 simulation or authorized submission. +#[allow(clippy::too_many_arguments)] +pub async fn execute_devnet_spl_token2022( + http_pool: &kb_onchain_transport::HttpEndpointPool, + store: &S, + profile: &kb_config::ProfileConfig, + workspace_root: &std::path::Path, + request: &crate::DevnetSplToken2022ExecutionRequest, + decoders: &[std::sync::Arc], + materializers: &[std::sync::Arc], + observer: &O, +) -> kb_core::Result +where + S: kb_store::RawTransactionStore + + kb_store::CoreExtractionStore + + kb_store::DecodePipelineStore + + Sync, + O: crate::SolanaExecutionObserver, +{ + 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), + }; + if !request.submit { + return std::result::Result::Ok(prepared.summary); + } + if !prepared.summary.simulation.success { + return std::result::Result::Err(kb_core::Error::new( + "execution_simulation_failed", + crate::simulation_failure_message(&prepared.summary.simulation), + )); + } + if let std::result::Result::Err(error) = validate_profile_wallet_signers( + prepared.unsigned.required_signer_pubkeys(), + prepared.summary.wallet.public_key.as_str(), + ) { + return std::result::Result::Err(error); + } + let send_evaluation = match kb_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 == kb_lib::ExSafetyDecision::Deny { + return std::result::Result::Err(kb_core::Error::new( + "execution_send_denied", + crate::violation_message(send_evaluation.violations.as_slice()), + )); + } + let signed = match prepared + .unsigned + .sign_after_simulation(&prepared.evidence, &[prepared.wallet.as_signer()]) + { + 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 = kb_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 kb_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!("Token-2022 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(kb_lib::ExApiExecutionCluster::Devnet)); + let confirmation_config = + match kb_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(), + kb_lib::ExApiExecutionCluster::Devnet, + &signature, + &confirmation_config, + ) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + diagnostic.diagnostics.push(format!("Token-2022 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, + kb_lib::ExApiExecutionConfirmationStatus::Confirmed + | kb_lib::ExApiExecutionConfirmationStatus::Finalized + ) { + diagnostic.diagnostics.push(format!( + "Token-2022 post-validation stopped at confirmation status {confirmation_status:?}" + )); + summary.post_execution = std::option::Option::Some(diagnostic); + return std::result::Result::Ok(summary); + } + 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!("Token-2022 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 Token-2022 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 crate::execute_core_extraction( + store, + &crate::CoreExtractionRequest { + source: crate::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!("Token-2022 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("Token-2022 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, + &[kb_program_ids::SPL_TOKEN2022_PROGRAM_ID], + decoders, + materializers, + observer, + ) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + diagnostic.diagnostics.push(format!("Token-2022 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); + summary.decode_replay = std::option::Option::Some(first_replay); + let filter = match kb_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 kb_store::DecodePipelineStore::list_materialized_events(store, &filter).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + diagnostic + .diagnostics + .push(format!("Token-2022 materialization query failed: {error}")); + summary.post_execution = std::option::Option::Some(diagnostic); + return std::result::Result::Ok(summary); + }, + }; + summary.materializations = rows + .into_iter() + .filter(|row| return row.source_decoder_name == "spl_token2022") + .collect(); + diagnostic.materialized = + !summary.plan.policy.post_execution_validation.materialization_required + || !summary.materializations.is_empty(); + let second_replay = match crate::replay_program( + store, + &signature, + true, + request.force_post_validation_replay, + &[kb_program_ids::SPL_TOKEN2022_PROGRAM_ID], + decoders, + materializers, + observer, + ) + .await + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + diagnostic + .diagnostics + .push(format!("Token-2022 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.processors.iter().all(|processor| { + return processor.failed == 0 + && processor.materialized_outputs == 0 + && processor.materialization_refused == 0; + }); + summary.idempotence_replay = std::option::Option::Some(second_replay); + if !idempotent { + diagnostic + .diagnostics + .push("second Token-2022 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( + "Token-2022 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); +} + +async fn prepare_execution( + http_pool: &kb_onchain_transport::HttpEndpointPool, + profile: &kb_config::ProfileConfig, + workspace_root: &std::path::Path, + request: &crate::DevnetSplToken2022ExecutionRequest, + observer: &O, +) -> kb_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, "token_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(kb_lib::ExApiExecutionCluster::Devnet) + { + return std::result::Result::Err(kb_core::Error::new( + "execution_cluster_mismatch", + format!( + "expected Devnet genesis hash but endpoint returned {} classified as {:?}", + genesis.genesis_hash, genesis.classified_cluster + ), + )); + } + let preflight_request = match preflight_request(request) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let readiness = match crate::inspect_token2022_preflight(http_pool, &preflight_request).await { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let wallet = match crate::load_profile_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 = kb_lib::MdPubkey(wallet_summary.public_key.clone()); + let balance = match http_pool + .get_balance_for_role( + request.query_role.as_str(), + &fee_payer, + &kb_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(kb_core::Error::new( + "execution_balance_insufficient", + format!( + "Devnet wallet balance {} is below the configured fee ceiling {}", + balance.lamports, profile.execution.max_fee_lamports + ), + )); + } + 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 plan_evaluation = match kb_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 == kb_lib::ExSafetyDecision::Deny { + return std::result::Result::Err(kb_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(), + &kb_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 kb_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 fee = match http_pool + .get_fee_for_message_for_role( + request.query_role.as_str(), + unsigned.message_base64().as_str(), + &kb_onchain_transport::GetFeeForMessageConfig::new( + kb_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), + }; + if fee.fee_lamports.is_none() { + return std::result::Result::Err(kb_core::Error::new( + "execution_fee_unavailable", + "getFeeForMessage returned null for the selected recent blockhash", + )); + } + 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 kb_onchain_transport::SimulateTransactionConfig::new( + kb_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, + "spl_token2022_simulation", + format!("simulating exact Token-2022 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( + kb_lib::ExApiExecutionCluster::Devnet, + kb_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 evidence = unsigned.bind_simulation(simulation.clone()); + let summary = crate::DevnetSplToken2022ExecutionSummary { + profile_name: profile.name.clone(), + cluster: kb_lib::ExApiExecutionCluster::Devnet, + genesis_hash: genesis.genesis_hash, + wallet: wallet_summary, + balance_lamports: balance.lamports, + stateful_preflight: readiness, + plan, + latest_blockhash, + fee, + simulation, + send_result: std::option::Option::None, + confirmation: std::option::Option::None, + 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, + }; + return std::result::Result::Ok(PreparedToken2022Execution { + wallet, + unsigned, + evidence, + summary, + }); +} + +fn preflight_request( + request: &crate::DevnetSplToken2022ExecutionRequest, +) -> kb_core::Result { + let operation = match &request.operation { + kb_lib::ExSplToken2022Operation::Instruction { value } => value.as_ref(), + kb_lib::ExSplToken2022Operation::Batch { instructions: _ } => { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token2022_batch_not_supported", + "Devnet Token-2022 validation scenarios require one non-batch operation", + )); + }, + }; + let mut requirements = std::vec::Vec::new(); + match operation { + kb_lib::ExSplTokenSingleOperation::MintToChecked { + mint, + destination, + authority, + amount: _, + decimals, + } => { + requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals))); + requirements.push(account_requirement( + "destination", + destination, + mint, + std::option::Option::None, + )); + if let std::result::Result::Err(error) = validate_single_authority(authority) { + return std::result::Result::Err(error); + } + }, + kb_lib::ExSplTokenSingleOperation::TransferChecked { + source, + mint, + destination, + authority, + amount: _, + decimals, + } => { + requirements.push(account_requirement( + "source", + source, + mint, + std::option::Option::Some(authority.authority.clone()), + )); + requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals))); + requirements.push(account_requirement( + "destination", + destination, + mint, + std::option::Option::None, + )); + if let std::result::Result::Err(error) = validate_single_authority(authority) { + return std::result::Result::Err(error); + } + }, + kb_lib::ExSplTokenSingleOperation::ApproveChecked { + source, + mint, + delegate: _, + authority, + amount: _, + decimals, + } => { + requirements.push(account_requirement( + "source", + source, + mint, + std::option::Option::Some(authority.authority.clone()), + )); + requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals))); + if let std::result::Result::Err(error) = validate_single_authority(authority) { + return std::result::Result::Err(error); + } + }, + kb_lib::ExSplTokenSingleOperation::Revoke { source, authority } => { + requirements.push(account_requirement( + "source", + source, + &kb_lib::MdPubkey(std::string::String::new()), + std::option::Option::Some(authority.authority.clone()), + )); + requirements[0].expected_mint = std::option::Option::None; + if let std::result::Result::Err(error) = validate_single_authority(authority) { + return std::result::Result::Err(error); + } + }, + kb_lib::ExSplTokenSingleOperation::BurnChecked { + source, + mint, + authority, + amount: _, + decimals, + } => { + requirements.push(account_requirement( + "source", + source, + mint, + std::option::Option::Some(authority.authority.clone()), + )); + requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals))); + if let std::result::Result::Err(error) = validate_single_authority(authority) { + return std::result::Result::Err(error); + } + }, + kb_lib::ExSplTokenSingleOperation::FreezeAccount { account, mint, authority } + | kb_lib::ExSplTokenSingleOperation::ThawAccount { account, mint, authority } => { + requirements.push(account_requirement( + "account", + account, + mint, + std::option::Option::None, + )); + requirements.push(mint_requirement("mint", mint, std::option::Option::None)); + if let std::result::Result::Err(error) = validate_single_authority(authority) { + return std::result::Result::Err(error); + } + }, + kb_lib::ExSplTokenSingleOperation::CloseAccount { account, destination: _, authority } => { + requirements.push(account_requirement( + "account", + account, + &kb_lib::MdPubkey(std::string::String::new()), + std::option::Option::Some(authority.authority.clone()), + )); + requirements[0].expected_mint = std::option::Option::None; + if let std::result::Result::Err(error) = validate_single_authority(authority) { + return std::result::Result::Err(error); + } + }, + _ => { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token2022_devnet_operation_unsupported", + format!( + "Token-2022 Devnet validation does not expose {}", + operation.operation_code() + ), + )); + }, + } + return std::result::Result::Ok(crate::Token2022PreflightRequest { + query_role: request.query_role.clone(), + min_context_slot: std::option::Option::None, + max_accounts: crate::MAX_TOKEN2022_PREFLIGHT_ACCOUNTS, + max_total_data_bytes: crate::MAX_TOKEN2022_PREFLIGHT_TOTAL_BYTES, + requirements, + elgamal_registry: std::option::Option::None, + }); +} + +fn validate_single_authority(authority: &kb_lib::ExSplTokenAuthority) -> kb_core::Result<()> { + if !authority.multisig_signers.is_empty() { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token2022_devnet_multisig_not_supported", + "the Devnet validation UI currently supports one profile-wallet authority", + )); + } + return std::result::Result::Ok(()); +} + +fn mint_requirement( + role: &str, + mint: &kb_lib::MdPubkey, + decimals: std::option::Option, +) -> crate::Token2022PreflightRequirement { + return crate::Token2022PreflightRequirement { + role: role.to_string(), + account: mint.clone(), + kind: kb_lib::DcToken2022StateKind::Mint, + max_data_bytes: 65_536, + expected_mint: std::option::Option::None, + expected_owner: std::option::Option::None, + expected_decimals: decimals, + required_extensions: std::vec::Vec::new(), + context: crate::Token2022StatefulContext::default(), + }; +} + +fn account_requirement( + role: &str, + account: &kb_lib::MdPubkey, + mint: &kb_lib::MdPubkey, + owner: std::option::Option, +) -> crate::Token2022PreflightRequirement { + return crate::Token2022PreflightRequirement { + role: role.to_string(), + account: account.clone(), + kind: kb_lib::DcToken2022StateKind::Account, + max_data_bytes: 65_536, + expected_mint: std::option::Option::Some(mint.clone()), + expected_owner: owner, + expected_decimals: std::option::Option::None, + required_extensions: std::vec::Vec::new(), + context: crate::Token2022StatefulContext::default(), + }; +} + +fn validate_profile( + profile: &kb_config::ProfileConfig, + request: &crate::DevnetSplToken2022ExecutionRequest, +) -> kb_core::Result<()> { + if profile.wallet.cluster != "devnet" { + return std::result::Result::Err(kb_core::Error::config( + "Token-2022 Devnet orchestration requires a Devnet wallet profile", + )); + } + if !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist { + return std::result::Result::Err(kb_core::Error::config( + "Token-2022 Devnet orchestration requires an enabled persistent temporary wallet", + )); + } + if !profile.execution.require_simulation { + return std::result::Result::Err(kb_core::Error::config( + "Token-2022 Devnet orchestration requires simulation", + )); + } + if request.submit && !profile.wallet.devnet_send_enabled { + return std::result::Result::Err(kb_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(kb_core::Error::config( + "Token-2022 Devnet submission requires explicit operator confirmation", + )); + } + return std::result::Result::Ok(()); +} + +fn build_plan( + profile: &kb_config::ProfileConfig, + request: &crate::DevnetSplToken2022ExecutionRequest, + fee_payer: kb_lib::MdPubkey, +) -> kb_core::Result { + let materialization_required = operation_requires_materialization(&request.operation); + let authorized_signers = std::vec![fee_payer.clone()]; + let intent = kb_lib::ExSplToken2022ExecutionIntent { + intent_id: request.intent_id.clone(), + fee_payer, + policy: kb_lib::ExApiExecutionPolicy { + cluster: kb_lib::ExApiExecutionClusterPolicy { + expected_cluster: kb_lib::ExApiExecutionCluster::Devnet, + allow_mainnet: false, + mainnet_confirmation: false, + }, + simulation: kb_lib::ExApiExecutionSimulationPolicy::Required, + blockhash: kb_lib::ExApiExecutionBlockhashPolicy { + kind: kb_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: kb_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, + dry_run: !request.submit, + post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy { + canonical_insert_required: true, + core_extraction_required: true, + decode_replay_required: true, + materialization_required, + }, + }, + operation: request.operation.clone(), + }; + return kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan( + &kb_lib::ExSplToken2022Executor, + &intent, + ); +} + +fn operation_requires_materialization(_operation: &kb_lib::ExSplToken2022Operation) -> bool { + return true; +} + +fn validate_profile_wallet_signers( + required_signers: &[std::string::String], + wallet_pubkey: &str, +) -> kb_core::Result<()> { + if required_signers.iter().any(|value| return value.as_str() != wallet_pubkey) { + return std::result::Result::Err(kb_core::Error::new( + "execution_spl_token2022_external_signer_unavailable", + format!( + "the Devnet Token orchestrator can sign only with profile wallet {}; required signers are {}", + wallet_pubkey, + required_signers.join(",") + ), + )); + } + return std::result::Result::Ok(()); +}