Files
khadhroony-bot3/migration/khadhroony-bot2-reference/kb_pipeline/src/solana_token_lifecycle.rs
2026-07-23 16:37:12 +02:00

1802 lines
74 KiB
Rust

// 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<std::string::String>) -> 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<crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationStep>,
}
/// 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<kb_model::Signature>,
/// 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<std::string::String>,
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<crate::solana_token_lifecycle::DevnetSplTokenLifecycleStepSummary>,
}
/// Creates and verifies the three raw Token-owned accounts required by one lifecycle.
pub async fn prepare_devnet_spl_token_lifecycle_accounts<O>(
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<crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationSummary>
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<S, O>(
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<dyn kb_decoder_api::InstructionDecoder>],
materializers: &[std::sync::Arc<dyn kb_materializer_api::EventMaterializer>],
observer: &O,
) -> kb_core::Result<crate::solana_token_lifecycle::DevnetSplTokenLifecycleSummary>
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<S, O>(
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<dyn kb_decoder_api::InstructionDecoder>],
materializers: &[std::sync::Arc<dyn kb_materializer_api::EventMaterializer>],
observer: &O,
) -> kb_core::Result<crate::solana_token_lifecycle::DevnetSplTokenLifecycleStepSummary>
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<O>(
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<kb_wallet::TemporaryWallet> {
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<O>(
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<crate::solana_token_lifecycle::DevnetSplTokenLifecyclePreparationStep>
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<kb_execution_api::PreparedExecutionPlan> {
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<u64> {
return match amount.0.parse::<u64>() {
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<std::vec::Vec<kb_executor_spl_token::SplTokenOperation>> {
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<crate::solana_token_lifecycle::DevnetSplTokenLifecycleStepSummary> {
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::<std::vec::Vec<_>>();
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::sync::Arc<dyn kb_decoder_api::InstructionDecoder>> =
std::vec![std::sync::Arc::new(kb_decoder_spl_token::SplTokenDecoder)];
let materializers: std::vec::Vec<
std::sync::Arc<dyn kb_materializer_api::EventMaterializer>,
> = 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::<u64>() {
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::<u8>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("{name} is invalid: {error}"),
},
std::result::Result::Err(_) => default,
};
}
}