Files
khadhroony-bot3/ks-pipeline-demo-scenarios/src/spl/token_2022/devnet_execution.rs
2026-08-10 09:28:57 +02:00

1046 lines
42 KiB
Rust

// file: ks-pipeline-demo-scenarios/src/spl/token_2022/devnet_execution.rs
// version: 9
//! Devnet Token-2022 execution with stateful and canonical post-validation.
/// Complete request for one Devnet Token-2022 execution.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DevnetSplToken2022ExecutionRequest {
/// Stable caller-provided execution identifier.
pub intent_id: std::string::String,
/// Endpoint role used for state, balance, blockhash, fee and hydration calls.
pub query_role: std::string::String,
/// Endpoint role used for simulation, submission and confirmation polling.
pub transaction_role: std::string::String,
/// Exact typed Token-2022 operation.
pub operation: ks_lib::ExSplToken2022Operation,
/// Explicitly authorizes signing and submission after successful simulation.
pub submit: bool,
/// Explicit operator confirmation required by the active profile.
pub operator_confirmed: bool,
/// Number of `getTransaction` retries after the first hydration attempt.
pub post_validation_max_retries: u32,
/// Replaces existing core and decode outputs for the submitted signature.
pub force_post_validation_replay: bool,
}
impl crate::DevnetSplToken2022ExecutionRequest {
/// Creates a conservative simulation-only request.
pub fn new(
intent_id: impl std::convert::Into<std::string::String>,
operation: ks_lib::ExSplToken2022Operation,
) -> Self {
return Self {
intent_id: intent_id.into(),
query_role: "http_queries".to_string(),
transaction_role: "http_transactions".to_string(),
operation,
submit: false,
operator_confirmed: false,
post_validation_max_retries: 10,
force_post_validation_replay: false,
};
}
/// Validates request-local bounds independently from one profile.
pub fn validate(&self) -> ks_core::Result<()> {
if self.intent_id.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Devnet Token-2022 execution intent id must not be empty",
));
}
if self.query_role.trim().is_empty() || self.transaction_role.trim().is_empty() {
return std::result::Result::Err(ks_core::Error::config(
"Devnet Token-2022 execution endpoint roles must not be empty",
));
}
if self.post_validation_max_retries > 20 {
return std::result::Result::Err(ks_core::Error::config(
"post-execution getTransaction retries must not exceed 20",
));
}
return std::result::Result::Ok(());
}
}
/// Complete result of one Devnet Token-2022 execution.
#[derive(Clone, Debug, PartialEq)]
pub struct DevnetSplToken2022ExecutionSummary {
/// Profile used by the orchestration.
pub profile_name: std::string::String,
/// Exact classified cluster.
pub cluster: ks_lib::ExApiExecutionCluster,
/// Genesis hash returned by the selected endpoint.
pub genesis_hash: std::string::String,
/// Non-secret persistent wallet description.
pub wallet: ks_wallet::WalletSummary,
/// Wallet balance observed before planning.
pub balance_lamports: u64,
/// Stateful readiness report produced before plan simulation.
pub stateful_preflight: ks_pipeline::Token2022PreflightReport,
/// Exact prepared Token plan.
pub plan: ks_lib::ExApiPreparedExecutionPlan,
/// Recent blockhash used by the exact transaction.
pub latest_blockhash: ks_onchain_transport::LatestBlockhashResult,
/// Fee estimate for the exact compiled message.
pub fee: ks_onchain_transport::FeeForMessageResult,
/// Exact simulation result bound to the compiled message.
pub simulation: ks_lib::ExApiExecutionSimulationResult,
/// Submission result when explicitly authorized.
pub send_result: std::option::Option<ks_lib::ExApiExecutionSendResult>,
/// Confirmation result when submitted.
pub confirmation: std::option::Option<ks_lib::ExApiExecutionConfirmationResult>,
/// Canonical hydration result for the exact signature.
pub backfill: std::option::Option<ks_pipeline::BackfillSummary>,
/// Core extraction result for the exact signature.
pub core_extraction: std::option::Option<ks_pipeline::CoreExtractionSummary>,
/// First Token decode and materialization replay.
pub decode_replay: std::option::Option<ks_pipeline::DecodeReplaySummary>,
/// Second replay proving idempotence for the same decoder version and input.
pub idempotence_replay: std::option::Option<ks_pipeline::DecodeReplaySummary>,
/// Exact materialized rows produced for the submitted Token transaction.
pub materializations: std::vec::Vec<ks_store::MaterializedEventQueryRow>,
/// Aggregated post-execution validation diagnostic.
pub post_execution: std::option::Option<ks_lib::ExApiPostExecutionDiagnostic>,
}
struct PreparedToken2022Execution {
wallet: ks_wallet::TemporaryWallet,
unsigned: ks_lib::ExSolanaUnsignedTransaction,
evidence: ks_lib::ExSolanaSimulationEvidence,
summary: crate::DevnetSplToken2022ExecutionSummary,
}
/// Simulates one Devnet Token-2022 operation after stateful preflight.
pub async fn simulate_devnet_spl_token_2022<O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetSplToken2022ExecutionRequest,
observer: &O,
) -> ks_core::Result<crate::DevnetSplToken2022ExecutionSummary>
where
O: crate::SolanaExecutionObserver,
{
if request.submit {
return std::result::Result::Err(ks_core::Error::config(
"simulate_devnet_spl_token_2022 requires submit=false",
));
}
let prepared =
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(prepared.summary);
}
/// Executes one Devnet Token-2022 simulation or authorized submission.
#[allow(clippy::too_many_arguments)]
pub async fn execute_devnet_spl_token_2022<S, O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
store: &S,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetSplToken2022ExecutionRequest,
decoders: &[std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>],
materializers: &[std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>],
observer: &O,
) -> ks_core::Result<crate::DevnetSplToken2022ExecutionSummary>
where
S: ks_store::RawTransactionStore
+ ks_store::CoreExtractionStore
+ ks_store::DecodePipelineStore
+ Sync,
O: crate::SolanaExecutionObserver,
{
let prepared =
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if !request.submit {
return std::result::Result::Ok(prepared.summary);
}
if !prepared.summary.simulation.success {
return std::result::Result::Err(ks_core::Error::new(
"execution_simulation_failed",
crate::simulation_failure_message(&prepared.summary.simulation),
));
}
if let std::result::Result::Err(error) = validate_profile_wallet_signers(
prepared.unsigned.required_signer_pubkeys(),
prepared.summary.wallet.public_key.as_str(),
) {
return std::result::Result::Err(error);
}
let send_evaluation = match ks_lib::ExSafetyChecker
.evaluate_send(&prepared.summary.plan, &prepared.summary.simulation)
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if send_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"execution_send_denied",
crate::violation_message(send_evaluation.violations.as_slice()),
));
}
let signed = match prepared
.unsigned
.sign_after_simulation(&prepared.evidence, &[prepared.wallet.as_signer()])
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::result::Result::Err(error) = signed.verify_signatures() {
return std::result::Result::Err(error);
}
let signature = signed.primary_signature().clone();
let mut summary = prepared.summary;
let mut diagnostic = ks_lib::ExApiPostExecutionDiagnostic {
signature: signature.clone(),
canonical_inserted: false,
core_extracted: false,
decode_replayed: false,
materialized: false,
diagnostics: std::vec::Vec::new(),
};
let send_config = match ks_onchain_transport::SendTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(summary.latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let sent = match http_pool
.send_transaction_for_role(
request.transaction_role.as_str(),
signed.transaction_base64().as_str(),
&signature,
&send_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Token-2022 submission failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
summary.send_result =
std::option::Option::Some(sent.to_execution_result(ks_lib::ExApiExecutionCluster::Devnet));
let confirmation_config =
match ks_onchain_transport::ConfirmTransactionConfig::from_execution_config(
&profile.execution,
std::option::Option::Some(summary.latest_blockhash.last_valid_block_height),
std::option::Option::Some(summary.latest_blockhash.context.slot),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let confirmation = match http_pool
.confirm_transaction_for_roles(
request.transaction_role.as_str(),
request.query_role.as_str(),
ks_lib::ExApiExecutionCluster::Devnet,
&signature,
&confirmation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Token-2022 confirmation failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
let confirmation_status = confirmation.status;
summary.confirmation = std::option::Option::Some(confirmation);
if !matches!(
confirmation_status,
ks_lib::ExApiExecutionConfirmationStatus::Confirmed
| ks_lib::ExApiExecutionConfirmationStatus::Finalized
) {
diagnostic.diagnostics.push(format!(
"Token-2022 post-validation stopped at confirmation status {confirmation_status:?}"
));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
let backfill = match crate::hydrate_signature(
http_pool,
store,
profile,
request.query_role.as_str(),
request.post_validation_max_retries,
observer,
&signature,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Token-2022 hydration failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
diagnostic.canonical_inserted = crate::canonical_available(&backfill);
summary.backfill = std::option::Option::Some(backfill);
if !diagnostic.canonical_inserted {
diagnostic.diagnostics.push(
"confirmed Token-2022 transaction was unavailable for canonical hydration".to_string(),
);
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
let extraction = match ks_pipeline::execute_core_extraction(
store,
&ks_pipeline::CoreExtractionRequest {
source: ks_pipeline::CoreExtractionSource::Signatures(std::vec![signature.0.clone()]),
limit: 1,
max_concurrent_extractions: 1,
force_replay: request.force_post_validation_replay,
},
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic
.diagnostics
.push(format!("Token-2022 core extraction failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
diagnostic.core_extracted = extraction.failed == 0
&& !extraction.cancelled
&& extraction.selected == 1
&& extraction.extracted.saturating_add(extraction.skipped) >= 1;
summary.core_extraction = std::option::Option::Some(extraction);
if !diagnostic.core_extracted {
diagnostic
.diagnostics
.push("Token-2022 core extraction did not complete".to_string());
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
let first_replay = match crate::replay_program(
store,
&signature,
false,
request.force_post_validation_replay,
&[ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID],
decoders,
materializers,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic.diagnostics.push(format!("Token-2022 decode replay failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
diagnostic.decode_replayed = crate::decode_completed(&first_replay);
summary.decode_replay = std::option::Option::Some(first_replay);
let filter = match ks_store::MaterializedEventFilter::new(
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(signature.0.clone()),
64,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let rows = match ks_store::DecodePipelineStore::list_materialized_events(store, &filter).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic
.diagnostics
.push(format!("Token-2022 materialization query failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
summary.materializations = rows
.into_iter()
.filter(|row| return row.source_decoder_name == "spl.token_2022")
.collect();
diagnostic.materialized =
!summary.plan.policy.post_execution_validation.materialization_required
|| !summary.materializations.is_empty();
let second_replay = match crate::replay_program(
store,
&signature,
true,
request.force_post_validation_replay,
&[ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID],
decoders,
materializers,
observer,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
diagnostic
.diagnostics
.push(format!("Token-2022 idempotence replay failed: {error}"));
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
},
};
let idempotent = second_replay.failed_inputs == 0
&& second_replay.processing_error_inputs == 0
&& second_replay.processors.iter().all(|processor| {
return processor.failed == 0
&& processor.processing_errors == 0
&& processor.materialized_outputs == 0
&& processor.materialization_refused == 0;
});
summary.idempotence_replay = std::option::Option::Some(second_replay);
if !idempotent {
diagnostic
.diagnostics
.push("second Token-2022 replay did not prove a clean idempotent skip".to_string());
} else if diagnostic.canonical_inserted
&& diagnostic.core_extracted
&& diagnostic.decode_replayed
&& diagnostic.materialized
{
diagnostic.diagnostics.push(
"Token-2022 completed canonical hydration, core extraction, decode, materialization and idempotence validation"
.to_string(),
);
}
summary.post_execution = std::option::Option::Some(diagnostic);
return std::result::Result::Ok(summary);
}
async fn prepare_execution<O>(
http_pool: &ks_onchain_transport::HttpEndpointPool,
profile: &ks_config::ProfileConfig,
workspace_root: &std::path::Path,
request: &crate::DevnetSplToken2022ExecutionRequest,
observer: &O,
) -> ks_core::Result<PreparedToken2022Execution>
where
O: crate::SolanaExecutionObserver,
{
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = validate_profile(profile, request) {
return std::result::Result::Err(error);
}
if let std::result::Result::Err(error) = crate::ensure_not_cancelled(observer, "token_validate")
{
return std::result::Result::Err(error);
}
let genesis = match http_pool.get_genesis_hash_for_role(request.query_role.as_str()).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if genesis.classified_cluster
!= std::option::Option::Some(ks_lib::ExApiExecutionCluster::Devnet)
{
return std::result::Result::Err(ks_core::Error::new(
"execution_cluster_mismatch",
format!(
"expected Devnet genesis hash but endpoint returned {} classified as {:?}",
genesis.genesis_hash, genesis.classified_cluster
),
));
}
let preflight_request = match preflight_request(request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let readiness =
match ks_pipeline::inspect_token_2022_preflight(http_pool, &preflight_request).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallet = match crate::load_profile_wallet(profile, workspace_root).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wallet_summary = wallet.summary();
let fee_payer = ks_lib::MdPubkey(wallet_summary.public_key.clone());
let balance = match http_pool
.get_balance_for_role(
request.query_role.as_str(),
&fee_payer,
&ks_onchain_transport::GetBalanceConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if balance.lamports < profile.execution.max_fee_lamports {
return std::result::Result::Err(ks_core::Error::new(
"execution_balance_insufficient",
format!(
"Devnet wallet balance {} is below the configured fee ceiling {}",
balance.lamports, profile.execution.max_fee_lamports
),
));
}
let plan = match build_plan(profile, request, fee_payer.clone()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let plan_evaluation = match ks_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if plan_evaluation.decision == ks_lib::ExSafetyDecision::Deny {
return std::result::Result::Err(ks_core::Error::new(
"execution_plan_denied",
crate::violation_message(plan_evaluation.violations.as_slice()),
));
}
let latest_blockhash = match http_pool
.get_latest_blockhash_for_role(
request.query_role.as_str(),
&ks_onchain_transport::GetLatestBlockhashConfig::confirmed(),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let unsigned = match ks_lib::executor_solana_build_legacy_transaction(
&plan,
latest_blockhash.blockhash.as_str(),
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let fee = match http_pool
.get_fee_for_message_for_role(
request.query_role.as_str(),
unsigned.message_base64().as_str(),
&ks_onchain_transport::GetFeeForMessageConfig::new(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
std::option::Option::Some(latest_blockhash.context.slot),
),
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if fee.fee_lamports.is_none() {
return std::result::Result::Err(ks_core::Error::new(
"execution_fee_unavailable",
"getFeeForMessage returned null for the selected recent blockhash",
));
}
let unsigned_base64 = match unsigned.transaction_base64() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation_config = match ks_onchain_transport::SimulateTransactionConfig::new(
ks_onchain_transport::RpcCommitmentLevel::Confirmed,
false,
false,
std::option::Option::Some(latest_blockhash.context.slot),
true,
std::option::Option::None,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::emit(
observer,
crate::SolanaExecutionProgressLevel::Info,
"spl_token_2022_simulation",
format!("simulating exact Token-2022 message {}", unsigned.message_hash()),
std::option::Option::None,
);
let simulation_rpc = match http_pool
.simulate_transaction_for_role(
request.transaction_role.as_str(),
unsigned_base64.as_str(),
&simulation_config,
)
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let simulation = simulation_rpc.to_execution_result(
ks_lib::ExApiExecutionCluster::Devnet,
ks_lib::ExApiExecutionBlockhashKind::Latest,
std::option::Option::Some(
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
),
std::option::Option::None,
std::option::Option::None,
std::option::Option::Some(&fee),
);
let evidence = unsigned.bind_simulation(simulation.clone());
let summary = crate::DevnetSplToken2022ExecutionSummary {
profile_name: profile.name.clone(),
cluster: ks_lib::ExApiExecutionCluster::Devnet,
genesis_hash: genesis.genesis_hash,
wallet: wallet_summary,
balance_lamports: balance.lamports,
stateful_preflight: readiness,
plan,
latest_blockhash,
fee,
simulation,
send_result: std::option::Option::None,
confirmation: std::option::Option::None,
backfill: std::option::Option::None,
core_extraction: std::option::Option::None,
decode_replay: std::option::Option::None,
idempotence_replay: std::option::Option::None,
materializations: std::vec::Vec::new(),
post_execution: std::option::Option::None,
};
return std::result::Result::Ok(PreparedToken2022Execution {
wallet,
unsigned,
evidence,
summary,
});
}
fn preflight_request(
request: &crate::DevnetSplToken2022ExecutionRequest,
) -> ks_core::Result<ks_pipeline::Token2022PreflightRequest> {
let operation = match &request.operation {
ks_lib::ExSplToken2022Operation::Instruction { value } => value.as_ref(),
ks_lib::ExSplToken2022Operation::Batch { instructions: _ } => {
return std::result::Result::Err(ks_core::Error::new(
"execution_spl_token_2022_batch_not_supported",
"Devnet Token-2022 validation scenarios require one non-batch operation",
));
},
};
let mut requirements = std::vec::Vec::new();
match operation {
ks_lib::ExSplTokenSingleOperation::MintToChecked {
mint,
destination,
authority,
amount: _,
decimals,
} => {
requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals)));
requirements.push(account_requirement(
"destination",
destination,
mint,
std::option::Option::None,
));
if let std::result::Result::Err(error) = validate_single_authority(authority) {
return std::result::Result::Err(error);
}
},
ks_lib::ExSplTokenSingleOperation::TransferChecked {
source,
mint,
destination,
authority,
amount: _,
decimals,
} => {
requirements.push(account_requirement(
"source",
source,
mint,
std::option::Option::Some(authority.authority.clone()),
));
requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals)));
requirements.push(account_requirement(
"destination",
destination,
mint,
std::option::Option::None,
));
if let std::result::Result::Err(error) = validate_single_authority(authority) {
return std::result::Result::Err(error);
}
},
ks_lib::ExSplTokenSingleOperation::ApproveChecked {
source,
mint,
delegate: _,
authority,
amount: _,
decimals,
} => {
requirements.push(account_requirement(
"source",
source,
mint,
std::option::Option::Some(authority.authority.clone()),
));
requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals)));
if let std::result::Result::Err(error) = validate_single_authority(authority) {
return std::result::Result::Err(error);
}
},
ks_lib::ExSplTokenSingleOperation::Revoke { source, authority } => {
requirements.push(account_requirement(
"source",
source,
&ks_lib::MdPubkey(std::string::String::new()),
std::option::Option::Some(authority.authority.clone()),
));
requirements[0].expected_mint = std::option::Option::None;
if let std::result::Result::Err(error) = validate_single_authority(authority) {
return std::result::Result::Err(error);
}
},
ks_lib::ExSplTokenSingleOperation::BurnChecked {
source,
mint,
authority,
amount: _,
decimals,
} => {
requirements.push(account_requirement(
"source",
source,
mint,
std::option::Option::Some(authority.authority.clone()),
));
requirements.push(mint_requirement("mint", mint, std::option::Option::Some(*decimals)));
if let std::result::Result::Err(error) = validate_single_authority(authority) {
return std::result::Result::Err(error);
}
},
ks_lib::ExSplTokenSingleOperation::FreezeAccount { account, mint, authority }
| ks_lib::ExSplTokenSingleOperation::ThawAccount { account, mint, authority } => {
requirements.push(account_requirement(
"account",
account,
mint,
std::option::Option::None,
));
requirements.push(mint_requirement("mint", mint, std::option::Option::None));
if let std::result::Result::Err(error) = validate_single_authority(authority) {
return std::result::Result::Err(error);
}
},
ks_lib::ExSplTokenSingleOperation::CloseAccount { account, destination: _, authority } => {
requirements.push(account_requirement(
"account",
account,
&ks_lib::MdPubkey(std::string::String::new()),
std::option::Option::Some(authority.authority.clone()),
));
requirements[0].expected_mint = std::option::Option::None;
if let std::result::Result::Err(error) = validate_single_authority(authority) {
return std::result::Result::Err(error);
}
},
ks_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
metadata,
update_authority: _,
mint,
mint_authority: _,
name: _,
symbol: _,
uri: _,
} => {
if metadata != mint {
return std::result::Result::Err(ks_core::Error::new(
"execution_spl_token_2022_metadata_mint_mismatch",
"Token Metadata Initialize on Token-2022 requires metadata and mint to be the same account",
));
}
requirements.push(metadata_mint_requirement("metadata", metadata, "metadata_pointer"));
},
ks_lib::ExSplTokenSingleOperation::UpdateTokenMetadataField {
metadata,
update_authority: _,
field: _,
value: _,
}
| ks_lib::ExSplTokenSingleOperation::RemoveTokenMetadataKey {
metadata,
update_authority: _,
key: _,
idempotent: _,
}
| ks_lib::ExSplTokenSingleOperation::UpdateTokenMetadataAuthority {
metadata,
current_authority: _,
new_authority: _,
}
| ks_lib::ExSplTokenSingleOperation::EmitTokenMetadata { metadata, start: _, end: _ } => {
requirements.push(metadata_mint_requirement("metadata", metadata, "token_metadata"));
},
_ => {
return std::result::Result::Err(ks_core::Error::new(
"execution_spl_token_2022_devnet_operation_unsupported",
format!(
"Token-2022 Devnet validation does not expose {}",
operation.operation_code()
),
));
},
}
return std::result::Result::Ok(ks_pipeline::Token2022PreflightRequest {
query_role: request.query_role.clone(),
min_context_slot: std::option::Option::None,
max_accounts: ks_pipeline::MAX_TOKEN_2022_PREFLIGHT_ACCOUNTS,
max_total_data_bytes: ks_pipeline::MAX_TOKEN_2022_PREFLIGHT_TOTAL_BYTES,
requirements,
elgamal_registry: std::option::Option::None,
});
}
fn validate_single_authority(authority: &ks_lib::ExSplTokenAuthority) -> ks_core::Result<()> {
if !authority.multisig_signers.is_empty() {
return std::result::Result::Err(ks_core::Error::new(
"execution_spl_token_2022_devnet_multisig_not_supported",
"the Devnet validation UI currently supports one profile-wallet authority",
));
}
return std::result::Result::Ok(());
}
fn mint_requirement(
role: &str,
mint: &ks_lib::MdPubkey,
decimals: std::option::Option<u8>,
) -> ks_pipeline::Token2022PreflightRequirement {
return ks_pipeline::Token2022PreflightRequirement {
role: role.to_string(),
account: mint.clone(),
kind: ks_lib::DcToken2022StateKind::Mint,
max_data_bytes: 65_536,
expected_mint: std::option::Option::None,
expected_owner: std::option::Option::None,
expected_decimals: decimals,
required_extensions: std::vec::Vec::new(),
context: ks_pipeline::Token2022StatefulContext::default(),
};
}
fn metadata_mint_requirement(
role: &str,
mint: &ks_lib::MdPubkey,
required_extension: &str,
) -> ks_pipeline::Token2022PreflightRequirement {
let mut requirement = mint_requirement(role, mint, std::option::Option::None);
requirement.required_extensions = std::vec![required_extension.to_string()];
return requirement;
}
fn account_requirement(
role: &str,
account: &ks_lib::MdPubkey,
mint: &ks_lib::MdPubkey,
owner: std::option::Option<ks_lib::MdPubkey>,
) -> ks_pipeline::Token2022PreflightRequirement {
return ks_pipeline::Token2022PreflightRequirement {
role: role.to_string(),
account: account.clone(),
kind: ks_lib::DcToken2022StateKind::Account,
max_data_bytes: 65_536,
expected_mint: std::option::Option::Some(mint.clone()),
expected_owner: owner,
expected_decimals: std::option::Option::None,
required_extensions: std::vec::Vec::new(),
context: ks_pipeline::Token2022StatefulContext::default(),
};
}
fn validate_profile(
profile: &ks_config::ProfileConfig,
request: &crate::DevnetSplToken2022ExecutionRequest,
) -> ks_core::Result<()> {
if profile.wallet.cluster != "devnet" {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 Devnet orchestration requires a Devnet wallet profile",
));
}
if !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 Devnet orchestration requires an enabled persistent temporary wallet",
));
}
if !profile.execution.require_simulation {
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 Devnet orchestration requires simulation",
));
}
if request.submit && !profile.execution.devnet_send_enabled {
return std::result::Result::Err(ks_core::Error::config(
"Devnet transaction submission is disabled by the wallet profile",
));
}
if request.submit
&& profile.execution.require_operator_confirmation
&& !request.operator_confirmed
{
return std::result::Result::Err(ks_core::Error::config(
"Token-2022 Devnet submission requires explicit operator confirmation",
));
}
return std::result::Result::Ok(());
}
fn build_plan(
profile: &ks_config::ProfileConfig,
request: &crate::DevnetSplToken2022ExecutionRequest,
fee_payer: ks_lib::MdPubkey,
) -> ks_core::Result<ks_lib::ExApiPreparedExecutionPlan> {
let materialization_required = operation_requires_materialization(&request.operation);
let authorized_signers = std::vec![fee_payer.clone()];
let intent = ks_lib::ExSplToken2022ExecutionIntent {
intent_id: request.intent_id.clone(),
fee_payer,
policy: ks_lib::ExApiExecutionPolicy {
cluster: ks_lib::ExApiExecutionClusterPolicy {
expected_cluster: ks_lib::ExApiExecutionCluster::Devnet,
allow_mainnet: false,
mainnet_confirmation: false,
},
simulation: ks_lib::ExApiExecutionSimulationPolicy::Required,
blockhash: ks_lib::ExApiExecutionBlockhashPolicy {
kind: ks_lib::ExApiExecutionBlockhashKind::Latest,
max_age_slots: std::option::Option::Some(
profile.execution.recent_blockhash_max_age_slots,
),
nonce_account: std::option::Option::None,
nonce_authority: std::option::Option::None,
},
cost_limit: ks_lib::ExApiExecutionCostLimit {
max_spend_lamports: std::option::Option::Some(
profile.execution.devnet_max_spend_lamports,
),
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
max_compute_unit_price_micro_lamports: std::option::Option::Some(
profile.execution.max_compute_unit_price_micro_lamports,
),
},
authorized_signers,
dry_run: !request.submit,
post_execution_validation: ks_lib::ExApiPostExecutionValidationPolicy {
canonical_insert_required: true,
core_extraction_required: true,
decode_replay_required: true,
materialization_required,
},
},
operation: request.operation.clone(),
};
return ks_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
&ks_lib::ExSplToken2022Executor,
&intent,
);
}
fn operation_requires_materialization(operation: &ks_lib::ExSplToken2022Operation) -> bool {
return !matches!(
operation,
ks_lib::ExSplToken2022Operation::Instruction { value }
if matches!(value.as_ref(), ks_lib::ExSplTokenSingleOperation::EmitTokenMetadata { .. })
);
}
fn validate_profile_wallet_signers(
required_signers: &[std::string::String],
wallet_pubkey: &str,
) -> ks_core::Result<()> {
if required_signers.iter().any(|value| return value.as_str() != wallet_pubkey) {
return std::result::Result::Err(ks_core::Error::new(
"execution_spl_token_2022_external_signer_unavailable",
format!(
"the Devnet Token orchestrator can sign only with profile wallet {}; required signers are {}",
wallet_pubkey,
required_signers.join(",")
),
));
}
return std::result::Result::Ok(());
}
#[cfg(test)]
mod tests {
fn pubkey(byte: u8) -> ks_lib::MdPubkey {
return ks_lib::MdPubkey(bs58::encode([byte; 32]).into_string());
}
fn request(
operation: ks_lib::ExSplTokenSingleOperation,
) -> crate::DevnetSplToken2022ExecutionRequest {
return crate::DevnetSplToken2022ExecutionRequest::new(
"metadata-preflight-test",
ks_lib::ExSplToken2022Operation::Instruction { value: std::boxed::Box::new(operation) },
);
}
#[test]
fn metadata_preflight_requires_pointer_before_initialize_and_metadata_afterward() {
let mint = pubkey(1);
let initialize = request(ks_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
metadata: mint.clone(),
update_authority: pubkey(2),
mint: mint.clone(),
mint_authority: pubkey(2),
name: "Token".to_string(),
symbol: "TOK".to_string(),
uri: "https://example.invalid/token.json".to_string(),
});
let initialize_preflight =
super::preflight_request(&initialize).unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
initialize_preflight.requirements[0].required_extensions,
vec!["metadata_pointer".to_string()]
);
let update = request(ks_lib::ExSplTokenSingleOperation::UpdateTokenMetadataField {
metadata: mint,
update_authority: pubkey(2),
field: ks_lib::ExSplTokenMetadataField::Key("campaign".to_string()),
value: "0.4.8-pre.012".to_string(),
});
let update_preflight =
super::preflight_request(&update).unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
update_preflight.requirements[0].required_extensions,
vec!["token_metadata".to_string()]
);
}
#[test]
fn metadata_initialize_rejects_external_metadata_account_and_emit_skips_materialization() {
let invalid = request(ks_lib::ExSplTokenSingleOperation::InitializeTokenMetadata {
metadata: pubkey(1),
update_authority: pubkey(2),
mint: pubkey(3),
mint_authority: pubkey(2),
name: "Token".to_string(),
symbol: "TOK".to_string(),
uri: "https://example.invalid/token.json".to_string(),
});
assert!(super::preflight_request(&invalid).is_err());
let emit = ks_lib::ExSplToken2022Operation::Instruction {
value: std::boxed::Box::new(ks_lib::ExSplTokenSingleOperation::EmitTokenMetadata {
metadata: pubkey(1),
start: std::option::Option::None,
end: std::option::Option::None,
}),
};
assert!(!super::operation_requires_materialization(&emit));
}
}