// file: kb_pipeline/src/solana_token_lifecycle.rs // version: 4 //! Controlled Devnet lifecycle for freshly prepared classic SPL Token accounts. const MINT_ACCOUNT_SPACE: u64 = 82; const TOKEN_ACCOUNT_SPACE: u64 = 165; /// Explicit request to prepare the three raw accounts used by one Devnet lifecycle. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DevnetSplTokenLifecyclePreparationRequest { /// Stable caller-provided preparation identifier. pub intent_id: std::string::String, /// Endpoint role used for cluster, rent, blockhash and account reads. pub query_role: std::string::String, /// Endpoint role used for simulation, submission and confirmation. pub transaction_role: std::string::String, /// Explicitly authorizes three rent-bearing System account creations. pub submit: bool, /// Explicit operator confirmation for the preparation spend. pub operator_confirmed: bool, } impl crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationRequest { /// Creates a conservative preparation request that cannot submit by default. pub fn new(intent_id: impl std::convert::Into) -> Self { return Self { intent_id: intent_id.into(), query_role: "http_queries".to_string(), transaction_role: "http_transactions".to_string(), submit: false, operator_confirmed: false, }; } fn validate(&self) -> kb_core::Result<()> { if self.intent_id.trim().is_empty() { return std::result::Result::Err(kb_core::Error::config( "Devnet SPL Token lifecycle preparation 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 SPL Token lifecycle preparation endpoint roles must not be empty", )); } if !self.submit || !self.operator_confirmed { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_preparation_authorization_required", "raw account preparation requires submit=true and explicit operator confirmation", )); } return std::result::Result::Ok(()); } } /// Result retained for one confirmed raw System account creation. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DevnetSplTokenLifecyclePreparationStep { /// Stable account role: `mint`, `source` or `destination`. pub account_role: std::string::String, /// Newly generated account public key. pub account: kb_model::Pubkey, /// Exact allocated data length. pub space: u64, /// Exact rent-exempt lamports funded into the account. pub rent_lamports: u64, /// Confirmed System account-creation transaction signature. pub signature: kb_model::Signature, /// Terminal confirmation status. pub confirmation_status: kb_execution_api::ExecutionConfirmationStatus, } /// Fresh raw accounts and identities ready for the controlled Token lifecycle. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DevnetSplTokenLifecyclePreparationSummary { /// Profile wallet that will own and administer the lifecycle accounts. pub authority: kb_model::Pubkey, /// Fresh uninitialized 82-byte Token-owned mint account. pub mint: kb_model::Pubkey, /// Fresh uninitialized 165-byte Token-owned source account. pub source: kb_model::Pubkey, /// Fresh uninitialized 165-byte Token-owned destination account. pub destination: kb_model::Pubkey, /// Fresh off-chain delegate identity used only by approve/revoke. pub delegate: kb_model::Pubkey, /// Ordered confirmed account-creation results. pub steps: std::vec::Vec, } /// Request for one destructive, explicitly authorized SPL Token Devnet lifecycle. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DevnetSplTokenLifecycleRequest { /// Stable caller-provided lifecycle identifier. pub intent_id: std::string::String, /// Endpoint role used for state reads and canonical hydration. pub query_role: std::string::String, /// Endpoint role used for simulation, submission and confirmation. pub transaction_role: std::string::String, /// Pre-created, rent-exempt, uninitialized 82-byte mint account. pub mint: kb_model::Pubkey, /// Pre-created, rent-exempt, uninitialized 165-byte source token account. pub source: kb_model::Pubkey, /// Pre-created, rent-exempt, uninitialized 165-byte destination token account. pub destination: kb_model::Pubkey, /// Delegate recorded by the approval and removed by the following revoke. pub delegate: kb_model::Pubkey, /// Mint authority and owner of both token accounts. pub authority: kb_model::Pubkey, /// Exact decimals used by all checked instructions. pub decimals: u8, /// Exact raw amount minted into the source account. pub mint_amount: kb_executor_spl_token::SplTokenAmount, /// Exact raw amount transferred from source to destination. pub transfer_amount: kb_executor_spl_token::SplTokenAmount, /// Exact raw allowance approved on the destination account. pub approve_amount: kb_executor_spl_token::SplTokenAmount, /// Explicitly authorizes every submitted lifecycle transaction. pub submit: bool, /// Explicit operator confirmation applied to every lifecycle transaction. pub operator_confirmed: bool, /// Number of canonical hydration retries after each confirmation. pub post_validation_max_retries: u32, /// First zero-based lifecycle step to execute. pub first_step_index: u8, /// Confirmed signature of the immediately preceding step when resuming. pub resume_predecessor_signature: std::option::Option, /// Bounded pause inserted between recovered or newly executed steps. pub inter_step_delay_ms: u64, } impl crate::solana_token_lifecycle::DevnetSplTokenLifecycleRequest { /// Creates a lifecycle request that remains non-submittable until explicitly authorized. pub fn new( intent_id: impl std::convert::Into, mint: kb_model::Pubkey, source: kb_model::Pubkey, destination: kb_model::Pubkey, delegate: kb_model::Pubkey, authority: kb_model::Pubkey, ) -> Self { return Self { intent_id: intent_id.into(), query_role: "http_queries".to_string(), transaction_role: "http_transactions".to_string(), mint, source, destination, delegate, authority, decimals: 9, mint_amount: kb_executor_spl_token::SplTokenAmount("10".to_string()), transfer_amount: kb_executor_spl_token::SplTokenAmount("4".to_string()), approve_amount: kb_executor_spl_token::SplTokenAmount("2".to_string()), submit: false, operator_confirmed: false, post_validation_max_retries: 10, first_step_index: 0, resume_predecessor_signature: std::option::Option::None, inter_step_delay_ms: 2_000, }; } /// Validates lifecycle-local bounds and amount conservation. pub fn validate(&self) -> kb_core::Result<()> { if self.intent_id.trim().is_empty() { return std::result::Result::Err(kb_core::Error::config( "Devnet SPL Token lifecycle 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 SPL Token lifecycle endpoint roles must not be empty", )); } if self.post_validation_max_retries > 20 { return std::result::Result::Err(kb_core::Error::config( "lifecycle post-execution retries must not exceed 20", )); } if self.first_step_index > 10 { return std::result::Result::Err(kb_core::Error::config( "lifecycle first step index must be between 0 and 10", )); } if self.inter_step_delay_ms > 30_000 { return std::result::Result::Err(kb_core::Error::config( "lifecycle inter-step delay must not exceed 30000 milliseconds", )); } if (self.first_step_index == 0 && self.resume_predecessor_signature.is_some()) || (self.first_step_index > 0 && self.resume_predecessor_signature.is_none()) { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_resume_contract_invalid", "a resumed lifecycle requires exactly the confirmed predecessor signature", )); } if let std::option::Option::Some(signature) = &self.resume_predecessor_signature { if let std::result::Result::Err(error) = kb_rpc::validate_transaction_signature_text( signature.0.as_str(), "lifecycle predecessor signature", ) { return std::result::Result::Err(error); } } if self.mint == self.source || self.mint == self.destination || self.source == self.destination { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_accounts_not_distinct", "lifecycle mint, source and destination accounts must be distinct", )); } if self.delegate == self.authority { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_delegate_is_authority", "lifecycle delegate must differ from the token-account authority", )); } let mint_amount = match crate::solana_token_lifecycle::canonical_amount(&self.mint_amount, "mint_amount") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let transfer_amount = match crate::solana_token_lifecycle::canonical_amount( &self.transfer_amount, "transfer_amount", ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let approve_amount = match crate::solana_token_lifecycle::canonical_amount( &self.approve_amount, "approve_amount", ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if mint_amount == 0 || transfer_amount == 0 || transfer_amount >= mint_amount || approve_amount == 0 || approve_amount > transfer_amount { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_amounts_invalid", "lifecycle requires mint > transfer > 0 and 0 < approval <= transfer", )); } return std::result::Result::Ok(()); } } /// Result retained for one fully post-validated lifecycle transaction. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DevnetSplTokenLifecycleStepSummary { /// Zero-based position in the controlled lifecycle. pub index: u8, /// Stable SPL Token operation code. pub operation_code: std::string::String, /// Confirmed transaction signature. pub signature: kb_model::Signature, /// Terminal confirmation status. pub confirmation_status: kb_execution_api::ExecutionConfirmationStatus, /// Number of exact SPL Token materializations returned for the signature. pub materialization_count: u32, /// Whether the second replay produced no failure, refusal or new output. pub idempotence_validated: bool, } /// Complete result of the eleven-step controlled lifecycle. #[derive(Clone, Debug, Eq, PartialEq)] pub struct DevnetSplTokenLifecycleSummary { /// Stable lifecycle identifier. pub intent_id: std::string::String, /// Mint initialized and exercised by the lifecycle. pub mint: kb_model::Pubkey, /// Source token account closed by the lifecycle. pub source: kb_model::Pubkey, /// Destination token account closed by the lifecycle. pub destination: kb_model::Pubkey, /// Ordered, fully validated lifecycle transactions. pub steps: std::vec::Vec, } /// Creates and verifies the three raw Token-owned accounts required by one lifecycle. pub async fn prepare_devnet_spl_token_lifecycle_accounts( http_pool: &kb_rpc::HttpEndpointPool, profile: &kb_config::ProfileConfig, workspace_root: &std::path::Path, request: &crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationRequest, 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 profile.wallet.cluster != "devnet" || !profile.wallet.devnet_send_enabled || !profile.execution.require_simulation { return std::result::Result::Err(kb_core::Error::config( "lifecycle account preparation requires a simulation-enabled Devnet send profile", )); } if profile.execution.require_operator_confirmation && !request.operator_confirmed { return std::result::Result::Err(kb_core::Error::config( "lifecycle account preparation requires explicit operator confirmation", )); } 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_execution_api::ExecutionCluster::Devnet) { return std::result::Result::Err(kb_core::Error::new( "execution_cluster_mismatch", "lifecycle account preparation endpoint is not Devnet", )); } let payer = match crate::solana_execution::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 authority = kb_model::Pubkey(payer.public_key()); let mint_wallet = match crate::solana_token_lifecycle::generated_wallet("token-lifecycle-mint") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let source_wallet = match crate::solana_token_lifecycle::generated_wallet("token-lifecycle-source") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let destination_wallet = match crate::solana_token_lifecycle::generated_wallet("token-lifecycle-destination") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let delegate_wallet = match crate::solana_token_lifecycle::generated_wallet("token-lifecycle-delegate") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let rent_config = kb_rpc::GetMinimumBalanceForRentExemptionConfig::confirmed(); let mint_rent = match http_pool .get_minimum_balance_for_rent_exemption_for_role( request.query_role.as_str(), MINT_ACCOUNT_SPACE, &rent_config, ) .await { std::result::Result::Ok(value) => value.minimum_balance_lamports, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let token_rent = match http_pool .get_minimum_balance_for_rent_exemption_for_role( request.query_role.as_str(), TOKEN_ACCOUNT_SPACE, &rent_config, ) .await { std::result::Result::Ok(value) => value.minimum_balance_lamports, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let doubled_token_rent = token_rent.checked_mul(2); let tripled_fee_ceiling = profile.execution.max_fee_lamports.checked_mul(3); let required_balance = match doubled_token_rent .and_then(|value| return mint_rent.checked_add(value)) .and_then(|value| { return tripled_fee_ceiling.and_then(|fees| return value.checked_add(fees)); }) { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_preparation_cost_overflow", "lifecycle preparation rent and fee ceiling overflow u64", )); }, }; let balance = match http_pool .get_balance_for_role( request.query_role.as_str(), &authority, &kb_rpc::GetBalanceConfig::confirmed(), ) .await { std::result::Result::Ok(value) => value.lamports, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if balance < required_balance { return std::result::Result::Err(kb_core::Error::new( "execution_balance_insufficient", format!( "lifecycle preparation requires at most {required_balance} lamports but wallet has {balance}" ), )); } let specifications = [ ("mint", &mint_wallet, MINT_ACCOUNT_SPACE, mint_rent), ("source", &source_wallet, TOKEN_ACCOUNT_SPACE, token_rent), ("destination", &destination_wallet, TOKEN_ACCOUNT_SPACE, token_rent), ]; let mut steps = std::vec::Vec::with_capacity(specifications.len()); for (account_role, wallet, space, rent_lamports) in specifications { let step = match crate::solana_token_lifecycle::create_raw_token_account( http_pool, profile, request, observer, &payer, wallet, account_role, space, rent_lamports, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; steps.push(step); } return std::result::Result::Ok( crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationSummary { authority, mint: kb_model::Pubkey(mint_wallet.public_key()), source: kb_model::Pubkey(source_wallet.public_key()), destination: kb_model::Pubkey(destination_wallet.public_key()), delegate: kb_model::Pubkey(delegate_wallet.public_key()), steps, }, ); } /// Executes initialize, mint, transfer, approve/revoke, burn and close on Devnet. #[allow(clippy::too_many_arguments)] pub async fn execute_devnet_spl_token_lifecycle( http_pool: &kb_rpc::HttpEndpointPool, store: &S, profile: &kb_config::ProfileConfig, workspace_root: &std::path::Path, request: &crate::solana_token_lifecycle::DevnetSplTokenLifecycleRequest, decoders: &[std::sync::Arc], materializers: &[std::sync::Arc], observer: &O, ) -> kb_core::Result where S: kb_store_core::RawTransactionStore + kb_store_core::CoreExtractionStore + kb_store_core::DecodePipelineStore + Sync, O: crate::SolanaExecutionObserver, { if let std::result::Result::Err(error) = request.validate() { return std::result::Result::Err(error); } if !request.submit || !request.operator_confirmed { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_authorization_required", "the destructive lifecycle requires submit=true and explicit operator confirmation", )); } let profile_wallet = match crate::solana_execution::load_profile_wallet(profile, workspace_root).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if profile_wallet.public_key() != request.authority.0 { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_authority_wallet_mismatch", "lifecycle authority must equal the selected profile wallet before the first mutation", )); } let operations = match crate::solana_token_lifecycle::lifecycle_operations(request) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let mut steps = std::vec::Vec::with_capacity(operations.len()); if request.first_step_index > 0 { let predecessor_index = usize::from(request.first_step_index.saturating_sub(1)); let predecessor_operation = match operations.get(predecessor_index) { std::option::Option::Some(value) => value.clone(), std::option::Option::None => { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_resume_index_missing", "lifecycle predecessor operation is unavailable", )); }, }; let predecessor_signature = match request.resume_predecessor_signature.as_ref() { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_resume_signature_missing", "lifecycle predecessor signature is required for recovery", )); }, }; let recovered = match crate::solana_token_lifecycle::recover_predecessor_step( http_pool, store, profile, request, predecessor_index, predecessor_operation, predecessor_signature, decoders, materializers, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; crate::solana_token_lifecycle::emit_completed_step(observer, &recovered, true); steps.push(recovered); } for (index, operation) in operations.into_iter().enumerate().skip(usize::from(request.first_step_index)) { if !steps.is_empty() && request.inter_step_delay_ms > 0 { tokio::time::sleep(std::time::Duration::from_millis(request.inter_step_delay_ms)).await; } let operation_code = operation.operation_code().to_string(); let mut step_request = crate::DevnetSplTokenExecutionRequest::new( format!("{}-{index:02}-{}", request.intent_id, operation_code), operation, ); step_request.query_role = request.query_role.clone(); step_request.transaction_role = request.transaction_role.clone(); step_request.submit = true; step_request.operator_confirmed = true; step_request.post_validation_max_retries = request.post_validation_max_retries; let summary = match crate::execute_devnet_spl_token( http_pool, store, profile, workspace_root, &step_request, decoders, materializers, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_step_failed", format!("lifecycle step {index} {operation_code} failed: {error}"), )); }, }; let step = match crate::solana_token_lifecycle::validated_step( index, operation_code.as_str(), &summary, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; crate::solana_token_lifecycle::emit_completed_step(observer, &step, false); steps.push(step); } return std::result::Result::Ok( crate::solana_token_lifecycle::DevnetSplTokenLifecycleSummary { intent_id: request.intent_id.clone(), mint: request.mint.clone(), source: request.source.clone(), destination: request.destination.clone(), steps, }, ); } #[allow(clippy::too_many_arguments)] async fn recover_predecessor_step( http_pool: &kb_rpc::HttpEndpointPool, store: &S, profile: &kb_config::ProfileConfig, lifecycle_request: &crate::solana_token_lifecycle::DevnetSplTokenLifecycleRequest, index: usize, operation: kb_executor_spl_token::SplTokenOperation, signature: &kb_model::Signature, decoders: &[std::sync::Arc], materializers: &[std::sync::Arc], observer: &O, ) -> kb_core::Result where S: kb_store_core::RawTransactionStore + kb_store_core::CoreExtractionStore + kb_store_core::DecodePipelineStore + Sync, O: crate::SolanaExecutionObserver, { let operation_code = operation.operation_code().to_string(); let expected_operation = crate::solana_token_lifecycle::materialized_operation_code(&operation).to_string(); let mut request = crate::DevnetSplTokenExecutionRequest::new( format!("{}-recover-{index:02}", lifecycle_request.intent_id), operation, ); request.query_role = lifecycle_request.query_role.clone(); request.transaction_role = lifecycle_request.transaction_role.clone(); request.post_validation_max_retries = lifecycle_request.post_validation_max_retries; let backfill = match crate::solana_token_execution::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) => return std::result::Result::Err(error), }; if !crate::solana_token_execution::canonical_available(&backfill) { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_resume_canonical_missing", "confirmed lifecycle predecessor is unavailable for canonical recovery", )); } 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: false, }, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if extraction.failed != 0 || extraction.cancelled || extraction.selected != 1 || extraction.extracted.saturating_add(extraction.skipped) < 1 { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_resume_extraction_incomplete", "lifecycle predecessor core extraction did not complete", )); } let first_replay = match crate::solana_token_execution::replay_program( store, signature, false, request.force_post_validation_replay, &[kb_program_ids::SPL_TOKEN_PROGRAM_ID], decoders, materializers, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if !crate::solana_token_execution::decode_completed(&first_replay) { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_resume_decode_incomplete", "lifecycle predecessor decode and materialization did not complete", )); } let filter = match kb_store_core::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_core::DecodePipelineStore::list_materialized_events(store, &filter).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let recovered_count = rows .iter() .filter(|row| { return row.source_decoder_name == "spl_token" && row.payload_json.get("operation").and_then(serde_json::Value::as_str) == std::option::Option::Some(expected_operation.as_str()); }) .count(); if recovered_count == 0 { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_resume_materialization_missing", format!( "lifecycle predecessor signature did not materialize expected operation {expected_operation}" ), )); } let second_replay = match crate::solana_token_execution::replay_program( store, signature, true, request.force_post_validation_replay, &[kb_program_ids::SPL_TOKEN_PROGRAM_ID], decoders, materializers, observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; 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; }); if !idempotent { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_resume_not_idempotent", "lifecycle predecessor second replay was not idempotent", )); } let materialization_count = match u32::try_from(recovered_count) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_materializations_overflow", "recovered lifecycle materialization count exceeds u32", )); }, }; let step_index = match u8::try_from(index) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_step_index_overflow", "recovered lifecycle step index exceeds u8", )); }, }; return std::result::Result::Ok( crate::solana_token_lifecycle::DevnetSplTokenLifecycleStepSummary { index: step_index, operation_code, signature: signature.clone(), confirmation_status: kb_execution_api::ExecutionConfirmationStatus::Confirmed, materialization_count, idempotence_validated: true, }, ); } fn materialized_operation_code(operation: &kb_executor_spl_token::SplTokenOperation) -> &str { return match operation { kb_executor_spl_token::SplTokenOperation::Instruction { value: kb_executor_spl_token::SplTokenSingleOperation::InitializeMint { .. }, } => "initialize_mint2", kb_executor_spl_token::SplTokenOperation::Instruction { value: kb_executor_spl_token::SplTokenSingleOperation::InitializeAccount { .. }, } => "initialize_account3", _ => match operation.operation_code().strip_prefix("spl_token.") { std::option::Option::Some(value) => value, std::option::Option::None => operation.operation_code(), }, }; } fn emit_completed_step( observer: &O, step: &crate::solana_token_lifecycle::DevnetSplTokenLifecycleStepSummary, recovered: bool, ) where O: crate::SolanaExecutionObserver, { crate::solana_execution::emit( observer, crate::SolanaExecutionProgressLevel::Info, "spl_token_lifecycle_step", format!( "lifecycle step {} {} {}", step.index, step.operation_code, if recovered { "recovered" } else { "completed" } ), std::option::Option::Some(step.signature.0.clone()), ); } fn generated_wallet(alias: &str) -> kb_core::Result { let alias = match kb_wallet::WalletAlias::parse(alias) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; return std::result::Result::Ok(kb_wallet::TemporaryWallet::generate(alias)); } #[allow(clippy::too_many_arguments)] async fn create_raw_token_account( http_pool: &kb_rpc::HttpEndpointPool, profile: &kb_config::ProfileConfig, request: &crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationRequest, observer: &O, payer: &kb_wallet::TemporaryWallet, account_wallet: &kb_wallet::TemporaryWallet, account_role: &str, space: u64, rent_lamports: u64, ) -> kb_core::Result where O: crate::SolanaExecutionObserver, { if let std::result::Result::Err(error) = crate::solana_execution::ensure_not_cancelled(observer, "token_lifecycle_prepare") { return std::result::Result::Err(error); } let account = kb_model::Pubkey(account_wallet.public_key()); let preflight = match http_pool .get_account_info_for_role( request.query_role.as_str(), &account, &kb_rpc::GetAccountInfoConfig::confirmed(), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if preflight.account.is_some() { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_preparation_account_exists", format!("generated lifecycle {account_role} account already exists"), )); } let payer_pubkey = kb_model::Pubkey(payer.public_key()); let plan = match crate::solana_token_lifecycle::build_account_creation_plan( profile, format!("{}-{account_role}", request.intent_id), payer_pubkey, account.clone(), space, rent_lamports, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let plan_evaluation = match kb_execution_safety::ExecutionSafetyChecker.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_execution_safety::ExecutionSafetyDecision::Deny { return std::result::Result::Err(kb_core::Error::new( "execution_plan_denied", crate::solana_execution::violation_message(plan_evaluation.violations.as_slice()), )); } let latest_blockhash = match http_pool .get_latest_blockhash_for_role( request.query_role.as_str(), &kb_rpc::GetLatestBlockhashConfig::confirmed(), ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let unsigned = match kb_execution_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_rpc::GetFeeForMessageConfig::new( kb_rpc::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", "lifecycle account-creation fee is unavailable", )); } 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_rpc::SimulateTransactionConfig::new( kb_rpc::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), }; 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_execution_api::ExecutionCluster::Devnet, kb_execution_api::ExecutionBlockhashKind::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 send_evaluation = match kb_execution_safety::ExecutionSafetyChecker.evaluate_send(&plan, &simulation) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if send_evaluation.decision == kb_execution_safety::ExecutionSafetyDecision::Deny { return std::result::Result::Err(kb_core::Error::new( "execution_send_denied", crate::solana_execution::violation_message(send_evaluation.violations.as_slice()), )); } let evidence = unsigned.bind_simulation(simulation); let signed = match unsigned .sign_after_simulation(&evidence, &[payer.as_signer(), account_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 send_config = match kb_rpc::SendTransactionConfig::from_execution_config( &profile.execution, std::option::Option::Some(latest_blockhash.context.slot), ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if let std::result::Result::Err(error) = http_pool .send_transaction_for_role( request.transaction_role.as_str(), signed.transaction_base64().as_str(), &signature, &send_config, ) .await { return std::result::Result::Err(error); } let confirmation_config = match kb_rpc::ConfirmTransactionConfig::from_execution_config( &profile.execution, std::option::Option::Some(latest_blockhash.last_valid_block_height), std::option::Option::Some(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_execution_api::ExecutionCluster::Devnet, &signature, &confirmation_config, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; if !matches!( confirmation.status, kb_execution_api::ExecutionConfirmationStatus::Confirmed | kb_execution_api::ExecutionConfirmationStatus::Finalized ) { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_preparation_unconfirmed", format!("lifecycle {account_role} account creation was not confirmed"), )); } let data_limit = match usize::try_from(space) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_preparation_space_overflow", "lifecycle account space exceeds usize", )); }, }; let account_config = match kb_rpc::GetAccountInfoConfig::confirmed_with_data(data_limit) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let account_info = match http_pool .get_account_info_for_role(request.query_role.as_str(), &account, &account_config) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let value = match account_info.account { std::option::Option::Some(value) => value, std::option::Option::None => { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_preparation_account_missing", format!("confirmed lifecycle {account_role} account is unavailable"), )); }, }; if value.owner.0 != kb_program_ids::SPL_TOKEN_PROGRAM_ID || value.executable || value.space != space || value.data.len() != data_limit || value.data.iter().any(|byte| return *byte != 0) || value.lamports < rent_lamports { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_preparation_account_invalid", format!("prepared lifecycle {account_role} account failed exact state validation"), )); } return std::result::Result::Ok( crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationStep { account_role: account_role.to_string(), account, space, rent_lamports, signature, confirmation_status: confirmation.status, }, ); } fn build_account_creation_plan( profile: &kb_config::ProfileConfig, intent_id: std::string::String, payer: kb_model::Pubkey, new_account: kb_model::Pubkey, space: u64, rent_lamports: u64, ) -> kb_core::Result { let intent = kb_executor_solana_core::SolanaCoreExecutionIntent { intent_id, fee_payer: payer.clone(), policy: kb_execution_api::ExecutionPolicy { cluster: kb_execution_api::ExecutionClusterPolicy { expected_cluster: kb_execution_api::ExecutionCluster::Devnet, allow_mainnet: false, mainnet_confirmation: false, }, simulation: kb_execution_api::ExecutionSimulationPolicy::Required, blockhash: kb_execution_api::ExecutionBlockhashPolicy { kind: kb_execution_api::ExecutionBlockhashKind::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_execution_api::ExecutionCostLimit { 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: std::vec![payer.clone(), new_account.clone()], dry_run: false, post_execution_validation: kb_execution_api::PostExecutionValidationPolicy { canonical_insert_required: false, core_extraction_required: false, decode_replay_required: false, materialization_required: false, }, }, operation: kb_executor_solana_core::SolanaCoreOperation::SystemCreateAccount { from: payer, new_account, lamports: rent_lamports, space, owner: kb_model::Pubkey(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()), }, }; return kb_execution_api::TypedInstructionExecutor::build_prepared_plan( &kb_executor_solana_core::SolanaCoreExecutor, &intent, ); } fn canonical_amount( amount: &kb_executor_spl_token::SplTokenAmount, field: &str, ) -> kb_core::Result { return match amount.0.parse::() { std::result::Result::Ok(value) if value.to_string() == amount.0 => { std::result::Result::Ok(value) }, std::result::Result::Ok(_) | std::result::Result::Err(_) => { std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_amount_invalid", format!("{field} must be a canonical u64 decimal string"), )) }, }; } fn lifecycle_operations( request: &crate::solana_token_lifecycle::DevnetSplTokenLifecycleRequest, ) -> kb_core::Result> { let mint_amount = match crate::solana_token_lifecycle::canonical_amount( &request.mint_amount, "mint_amount", ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let transfer_amount = match crate::solana_token_lifecycle::canonical_amount( &request.transfer_amount, "transfer_amount", ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; let authority = kb_executor_spl_token::SplTokenAuthority { authority: request.authority.clone(), multisig_signers: std::vec::Vec::new(), }; let source_burn = mint_amount.saturating_sub(transfer_amount).to_string(); let operations = std::vec![ kb_executor_spl_token::SplTokenSingleOperation::InitializeMint { mint: request.mint.clone(), mint_authority: request.authority.clone(), freeze_authority: std::option::Option::Some(request.authority.clone()), decimals: request.decimals, }, kb_executor_spl_token::SplTokenSingleOperation::InitializeAccount { account: request.source.clone(), mint: request.mint.clone(), owner: request.authority.clone(), }, kb_executor_spl_token::SplTokenSingleOperation::InitializeAccount { account: request.destination.clone(), mint: request.mint.clone(), owner: request.authority.clone(), }, kb_executor_spl_token::SplTokenSingleOperation::MintToChecked { mint: request.mint.clone(), destination: request.source.clone(), authority: authority.clone(), amount: request.mint_amount.clone(), decimals: request.decimals, }, kb_executor_spl_token::SplTokenSingleOperation::TransferChecked { source: request.source.clone(), mint: request.mint.clone(), destination: request.destination.clone(), authority: authority.clone(), amount: request.transfer_amount.clone(), decimals: request.decimals, }, kb_executor_spl_token::SplTokenSingleOperation::ApproveChecked { source: request.destination.clone(), mint: request.mint.clone(), delegate: request.delegate.clone(), authority: authority.clone(), amount: request.approve_amount.clone(), decimals: request.decimals, }, kb_executor_spl_token::SplTokenSingleOperation::Revoke { source: request.destination.clone(), authority: authority.clone(), }, kb_executor_spl_token::SplTokenSingleOperation::BurnChecked { source: request.source.clone(), mint: request.mint.clone(), authority: authority.clone(), amount: kb_executor_spl_token::SplTokenAmount(source_burn), decimals: request.decimals, }, kb_executor_spl_token::SplTokenSingleOperation::BurnChecked { source: request.destination.clone(), mint: request.mint.clone(), authority: authority.clone(), amount: request.transfer_amount.clone(), decimals: request.decimals, }, kb_executor_spl_token::SplTokenSingleOperation::CloseAccount { account: request.source.clone(), destination: request.authority.clone(), authority: authority.clone(), }, kb_executor_spl_token::SplTokenSingleOperation::CloseAccount { account: request.destination.clone(), destination: request.authority.clone(), authority, }, ]; return std::result::Result::Ok( operations .into_iter() .map(|value| return kb_executor_spl_token::SplTokenOperation::Instruction { value }) .collect(), ); } fn validated_step( index: usize, operation_code: &str, summary: &crate::DevnetSplTokenExecutionSummary, ) -> kb_core::Result { let send = match summary.send_result.as_ref() { std::option::Option::Some(value) if value.submitted => value, std::option::Option::Some(_) | std::option::Option::None => { return std::result::Result::Err(crate::solana_token_lifecycle::incomplete_step( index, operation_code, "submission", )); }, }; let confirmation = match summary.confirmation.as_ref() { std::option::Option::Some(value) if matches!( value.status, kb_execution_api::ExecutionConfirmationStatus::Confirmed | kb_execution_api::ExecutionConfirmationStatus::Finalized ) => { value }, std::option::Option::Some(_) | std::option::Option::None => { return std::result::Result::Err(crate::solana_token_lifecycle::incomplete_step( index, operation_code, "confirmation", )); }, }; match summary.post_execution.as_ref() { std::option::Option::Some(value) if value.canonical_inserted && value.core_extracted && value.decode_replayed && value.materialized => {}, std::option::Option::Some(_) | std::option::Option::None => { return std::result::Result::Err(crate::solana_token_lifecycle::incomplete_step( index, operation_code, "post_validation", )); }, }; let idempotence = summary.idempotence_replay.as_ref().is_some_and(|value| { return value.failed_inputs == 0 && value.processors.iter().all(|processor| { return processor.failed == 0 && processor.materialized_outputs == 0 && processor.materialization_refused == 0; }); }); if !idempotence || summary.materializations.is_empty() { return std::result::Result::Err(crate::solana_token_lifecycle::incomplete_step( index, operation_code, "materialization_idempotence", )); } let materialization_count = match u32::try_from(summary.materializations.len()) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_materializations_overflow", "lifecycle materialization count exceeds u32", )); }, }; let step_index = match u8::try_from(index) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => { return std::result::Result::Err(kb_core::Error::new( "execution_spl_token_lifecycle_step_index_overflow", "lifecycle step index exceeds u8", )); }, }; return std::result::Result::Ok( crate::solana_token_lifecycle::DevnetSplTokenLifecycleStepSummary { index: step_index, operation_code: operation_code.to_string(), signature: send.signature.clone(), confirmation_status: confirmation.status, materialization_count, idempotence_validated: true, }, ); } fn incomplete_step(index: usize, operation_code: &str, stage: &str) -> kb_core::Error { return kb_core::Error::new( "execution_spl_token_lifecycle_post_validation_incomplete", format!("lifecycle step {index} {operation_code} did not validate {stage}"), ); } #[cfg(test)] mod tests { struct LifecycleTestObserver; impl crate::BackfillObserver for LifecycleTestObserver { fn on_progress(&self, _event: &crate::BackfillProgressEvent) { return; } fn is_cancelled(&self) -> bool { return false; } } impl crate::CoreExtractionObserver for LifecycleTestObserver { fn on_progress(&self, _event: &crate::CoreExtractionProgressEvent) { return; } fn is_cancelled(&self) -> bool { return false; } } impl crate::DecodeReplayObserver for LifecycleTestObserver { fn on_progress(&self, _event: &crate::DecodeReplayProgressEvent) { return; } fn is_cancelled(&self) -> bool { return false; } } impl crate::SolanaExecutionObserver for LifecycleTestObserver { fn on_execution_progress(&self, event: &crate::SolanaExecutionProgressEvent) { if event.stage == "spl_token_lifecycle_step" { let signature = match event.signature.as_deref() { std::option::Option::Some(value) => value, std::option::Option::None => "missing", }; println!("SPL Token lifecycle progress={} signature={}", event.message, signature,); } return; } fn is_execution_cancelled(&self) -> bool { return false; } } fn key(value: &str) -> kb_model::Pubkey { return kb_model::Pubkey(value.to_string()); } fn request() -> crate::DevnetSplTokenLifecycleRequest { return crate::DevnetSplTokenLifecycleRequest::new( "lifecycle-1", crate::solana_token_lifecycle::tests::key(kb_program_ids::STAKE_PROGRAM_ID), crate::solana_token_lifecycle::tests::key(kb_program_ids::VOTE_PROGRAM_ID), crate::solana_token_lifecycle::tests::key(kb_program_ids::CONFIG_PROGRAM_ID), crate::solana_token_lifecycle::tests::key(kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID), crate::solana_token_lifecycle::tests::key(kb_program_ids::SYSTEM_PROGRAM_ID), ); } #[test] fn conservative_request_requires_explicit_destructive_authorization() { let request = crate::solana_token_lifecycle::tests::request(); assert!(!request.submit); assert!(!request.operator_confirmed); assert!(request.validate().is_ok()); } #[test] fn lifecycle_operations_preserve_order_and_conserve_raw_amounts() { let request = crate::solana_token_lifecycle::tests::request(); let operations = match super::lifecycle_operations(&request) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("lifecycle construction failed: {error}"), }; let codes = operations .iter() .map(|value| return value.operation_code()) .collect::>(); assert_eq!( codes, std::vec![ "spl_token.initialize_mint", "spl_token.initialize_account", "spl_token.initialize_account", "spl_token.mint_to_checked", "spl_token.transfer_checked", "spl_token.approve_checked", "spl_token.revoke", "spl_token.burn_checked", "spl_token.burn_checked", "spl_token.close_account", "spl_token.close_account", ], ); let source_burn = match &operations[7] { kb_executor_spl_token::SplTokenOperation::Instruction { value: kb_executor_spl_token::SplTokenSingleOperation::BurnChecked { amount, .. }, } => amount.0.as_str(), _ => panic!("source burn step missing"), }; assert_eq!(source_burn, "6"); } #[test] fn invalid_amounts_aliases_and_delegate_fail_closed() { let mut request = crate::solana_token_lifecycle::tests::request(); request.transfer_amount = kb_executor_spl_token::SplTokenAmount("10".to_string()); assert!(request.validate().is_err()); request.transfer_amount = kb_executor_spl_token::SplTokenAmount("04".to_string()); assert!(request.validate().is_err()); request.transfer_amount = kb_executor_spl_token::SplTokenAmount("4".to_string()); request.delegate = request.authority.clone(); assert!(request.validate().is_err()); request.delegate = crate::solana_token_lifecycle::tests::key(kb_program_ids::COMPUTE_BUDGET_PROGRAM_ID); request.destination = request.source.clone(); assert!(request.validate().is_err()); } #[test] fn account_preparation_requires_explicit_spend_authorization() { let mut request = crate::DevnetSplTokenLifecyclePreparationRequest::new("prepare-1"); assert!(request.validate().is_err()); request.submit = true; assert!(request.validate().is_err()); request.operator_confirmed = true; assert!(request.validate().is_ok()); } #[test] fn account_creation_plan_uses_exact_token_owner_space_spend_and_signers() { let profile = crate::solana_token_lifecycle::tests::local_devnet_profile(); let payer = crate::solana_token_lifecycle::tests::key(kb_program_ids::SYSTEM_PROGRAM_ID); let account = crate::solana_token_lifecycle::tests::key(kb_program_ids::STAKE_PROGRAM_ID); let plan = match super::build_account_creation_plan( &profile, "prepare-mint".to_string(), payer.clone(), account.clone(), super::MINT_ACCOUNT_SPACE, 1_461_600, ) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("account creation plan failed: {error}"), }; assert_eq!(plan.operation_code, "solana_core.system.create_account"); assert_eq!(plan.requested_spend_lamports, 1_461_600); assert_eq!(plan.instructions.len(), 1); assert_eq!(plan.instructions[0].accounts.len(), 2); assert_eq!(plan.instructions[0].accounts[0].pubkey, payer); assert_eq!(plan.instructions[0].accounts[1].pubkey, account); assert_eq!(plan.required_signers.len(), 2); assert_eq!(plan.policy.authorized_signers.len(), 2); assert!(!plan.policy.dry_run); } #[test] fn resume_requires_exact_predecessor_signature_and_bounded_delay() { let mut request = crate::solana_token_lifecycle::tests::request(); request.first_step_index = 3; assert!(request.validate().is_err()); request.resume_predecessor_signature = std::option::Option::Some(kb_model::Signature( "3yxsCf8tc1H2P9E3m6Kgu7J3XVeBw7wePuRTHvCaidQ74bsa2TP5xg4hpXAw1jkRSB3LrkLrXgx5eqvCQqRsXcfq".to_string(), )); assert!(request.validate().is_ok()); request.inter_step_delay_ms = 30_001; assert!(request.validate().is_err()); request.inter_step_delay_ms = 2_000; request.first_step_index = 0; assert!(request.validate().is_err()); } #[test] fn recovery_uses_current_initialization_wire_names() { let request = crate::solana_token_lifecycle::tests::request(); let operations = match super::lifecycle_operations(&request) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("lifecycle construction failed: {error}"), }; assert_eq!(super::materialized_operation_code(&operations[0]), "initialize_mint2"); assert_eq!(super::materialized_operation_code(&operations[2]), "initialize_account3"); assert_eq!(super::materialized_operation_code(&operations[3]), "mint_to_checked"); } #[tokio::test] async fn optional_devnet_controlled_lifecycle_from_env() { if std::env::var("KB_DEVNET_SPL_TOKEN_LIFECYCLE_TEST").ok().as_deref() != std::option::Option::Some("1") { return; } if std::env::var("KB_DEVNET_SPL_TOKEN_LIFECYCLE_SUBMIT").ok().as_deref() != std::option::Option::Some("1") { panic!("controlled lifecycle requires KB_DEVNET_SPL_TOKEN_LIFECYCLE_SUBMIT=1"); } let mut profile = crate::solana_token_lifecycle::tests::local_devnet_profile(); if let std::result::Result::Ok(directory) = std::env::var("KB_DEVNET_WALLET_DIR") { profile.wallet.wallet_dir = directory; } let database_url = match std::env::var("KB_POSTGRES_TEST_URL") { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { panic!("KB_POSTGRES_TEST_URL is required for lifecycle submission: {error}"); }, }; profile.database.backend = "postgres".to_string(); profile.database.postgres.url = database_url; let pool = match kb_rpc::HttpEndpointPool::from_profile(&profile) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"), }; let store = match kb_store_pg::PostgresStore::connect_from_profile_config(&profile).await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("PostgreSQL connection failed: {error}"), }; if let std::result::Result::Err(error) = store.initialize_store_schema().await { panic!("PostgreSQL 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 observer = crate::solana_token_lifecycle::tests::LifecycleTestObserver; let prepared_accounts = if std::env::var("KB_DEVNET_SPL_TOKEN_LIFECYCLE_PREPARE") .ok() .as_deref() == std::option::Option::Some("1") { let mut preparation = crate::DevnetSplTokenLifecyclePreparationRequest::new(format!( "devnet-token-lifecycle-prepare-{}", uuid::Uuid::new_v4() )); preparation.submit = true; preparation.operator_confirmed = true; let summary = match crate::prepare_devnet_spl_token_lifecycle_accounts( &pool, &profile, workspace_root, &preparation, &observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => { panic!("Devnet Token lifecycle account preparation failed: {error}"); }, }; for step in &summary.steps { println!( "SPL Token lifecycle preparation role={} account={} space={} rent={} signature={} status={:?}", step.account_role, step.account.0, step.space, step.rent_lamports, step.signature.0, step.confirmation_status, ); } std::option::Option::Some(summary) } else { std::option::Option::None }; if prepared_accounts.is_some() { tokio::time::sleep(std::time::Duration::from_millis( crate::solana_token_lifecycle::tests::optional_u64_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_PREPARATION_SETTLE_DELAY_MS", 10_000, ), )) .await; } let (mint, source, destination, delegate, authority) = match prepared_accounts.as_ref() { std::option::Option::Some(value) => ( value.mint.clone(), value.source.clone(), value.destination.clone(), value.delegate.clone(), value.authority.clone(), ), std::option::Option::None => ( crate::solana_token_lifecycle::tests::required_pubkey_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_MINT", ), crate::solana_token_lifecycle::tests::required_pubkey_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_SOURCE", ), crate::solana_token_lifecycle::tests::required_pubkey_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_DESTINATION", ), crate::solana_token_lifecycle::tests::required_pubkey_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_DELEGATE", ), crate::solana_token_lifecycle::tests::required_pubkey_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_AUTHORITY", ), ), }; let mut request = crate::DevnetSplTokenLifecycleRequest::new( format!("devnet-token-lifecycle-{}", uuid::Uuid::new_v4()), mint, source, destination, delegate, authority, ); request.decimals = crate::solana_token_lifecycle::tests::optional_u8_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_DECIMALS", 9, ); request.mint_amount = kb_executor_spl_token::SplTokenAmount( crate::solana_token_lifecycle::tests::optional_string_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_MINT_AMOUNT", "10", ), ); request.transfer_amount = kb_executor_spl_token::SplTokenAmount( crate::solana_token_lifecycle::tests::optional_string_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_TRANSFER_AMOUNT", "4", ), ); request.approve_amount = kb_executor_spl_token::SplTokenAmount( crate::solana_token_lifecycle::tests::optional_string_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_APPROVE_AMOUNT", "2", ), ); request.submit = true; request.operator_confirmed = true; request.post_validation_max_retries = 20; request.first_step_index = crate::solana_token_lifecycle::tests::optional_u8_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_FIRST_STEP_INDEX", 0, ); request.resume_predecessor_signature = if request.first_step_index > 0 { std::option::Option::Some(kb_model::Signature( crate::solana_token_lifecycle::tests::required_string_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_RESUME_PREDECESSOR_SIGNATURE", ), )) } else { std::option::Option::None }; request.inter_step_delay_ms = crate::solana_token_lifecycle::tests::optional_u64_env( "KB_DEVNET_SPL_TOKEN_LIFECYCLE_INTER_STEP_DELAY_MS", 2_000, ); let decoders: std::vec::Vec> = std::vec![std::sync::Arc::new(kb_decoder_spl_token::SplTokenDecoder)]; let materializers: std::vec::Vec< std::sync::Arc, > = std::vec![std::sync::Arc::new( kb_materializer_token_accounts::TokenAccountsMaterializer, )]; let summary = match crate::execute_devnet_spl_token_lifecycle( &pool, &store, &profile, workspace_root, &request, decoders.as_slice(), materializers.as_slice(), &observer, ) .await { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("Devnet Token lifecycle failed: {error}"), }; let expected_step_count = if request.first_step_index == 0 { 11 } else { 12_usize.saturating_sub(usize::from(request.first_step_index)) }; assert_eq!(summary.steps.len(), expected_step_count); assert!(summary.steps.iter().all(|step| { return step.idempotence_validated && step.materialization_count > 0; })); for step in summary.steps { println!( "SPL Token lifecycle step={} operation={} signature={} status={:?} materializations={} idempotent={}", step.index, step.operation_code, step.signature.0, step.confirmation_status, step.materialization_count, step.idempotence_validated, ); } } fn local_devnet_profile() -> kb_config::ProfileConfig { let config = match kb_config::parse_config_json(include_str!("../../config/example.config.json")) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("example config parse failed: {error}"), }; for profile in config.profiles { if profile.name == "local_devnet" { return profile; } } panic!("local_devnet profile missing"); } fn required_pubkey_env(name: &str) -> kb_model::Pubkey { return match std::env::var(name) { std::result::Result::Ok(value) => kb_model::Pubkey(value), std::result::Result::Err(error) => panic!("{name} is required: {error}"), }; } fn optional_string_env(name: &str, default: &str) -> std::string::String { return match std::env::var(name) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => default.to_string(), }; } fn required_string_env(name: &str) -> std::string::String { return match std::env::var(name) { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("{name} is required: {error}"), }; } fn optional_u64_env(name: &str, default: u64) -> u64 { return match std::env::var(name) { std::result::Result::Ok(value) => match value.parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("{name} is invalid: {error}"), }, std::result::Result::Err(_) => default, }; } fn optional_u8_env(name: &str, default: u8) -> u8 { return match std::env::var(name) { std::result::Result::Ok(value) => match value.parse::() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => panic!("{name} is invalid: {error}"), }, std::result::Result::Err(_) => default, }; } }