626 lines
26 KiB
Rust
626 lines
26 KiB
Rust
// file: kb-app-demo-desktop/src/demo_spl_ata.rs
|
|
// version: 13
|
|
|
|
//! Thin Tauri adapter for ATA execution, derivation and lifecycle journal reads.
|
|
|
|
use ts_rs::TS; // rust-rules: derive-import
|
|
|
|
/// Request for one representative ATA creation on Devnet.
|
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaRequest.ts"
|
|
)]
|
|
pub(crate) struct DemoExecutionSplAtaRequest {
|
|
/// Selected Devnet profile.
|
|
pub(crate) profile_name: std::string::String,
|
|
/// Wallet owner used by canonical derivation.
|
|
pub(crate) wallet_owner: std::string::String,
|
|
/// Mint account.
|
|
pub(crate) mint: std::string::String,
|
|
/// `classic` or `token_2022`.
|
|
pub(crate) token_program: std::string::String,
|
|
/// `create` or `create_idempotent`.
|
|
pub(crate) mode: std::string::String,
|
|
/// Whether to sign and submit after simulation.
|
|
pub(crate) submit: bool,
|
|
/// Explicit operator confirmation.
|
|
pub(crate) operator_confirmed: bool,
|
|
/// Whether post-execution outputs should be force-replayed.
|
|
pub(crate) force_post_validation_replay: bool,
|
|
}
|
|
|
|
/// Request used to derive the representative wallet ATA before execution.
|
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationRequest.ts"
|
|
)]
|
|
pub(crate) struct DemoSplAtaDerivationRequest {
|
|
/// Selected Devnet profile.
|
|
pub(crate) profile_name: std::string::String,
|
|
/// Mint account.
|
|
pub(crate) mint: std::string::String,
|
|
/// `classic` or `token_2022`.
|
|
pub(crate) token_program: std::string::String,
|
|
}
|
|
|
|
/// Readonly payer, wallet, Token Program and derived ATA values.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaDerivationPayload.ts"
|
|
)]
|
|
pub(crate) struct DemoSplAtaDerivationPayload {
|
|
/// Profile wallet used as payer.
|
|
pub(crate) payer: std::string::String,
|
|
/// Wallet owner selected by the representative panel.
|
|
pub(crate) wallet_owner: std::string::String,
|
|
/// Exact Token Program ID.
|
|
pub(crate) token_program_id: std::string::String,
|
|
/// Canonically derived ATA.
|
|
pub(crate) associated_token_account: std::string::String,
|
|
}
|
|
|
|
/// UI-safe ATA execution result.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoExecutionSplAtaSummaryPayload.ts"
|
|
)]
|
|
pub(crate) struct DemoExecutionSplAtaSummaryPayload {
|
|
/// Stable operation code.
|
|
pub(crate) operation: std::string::String,
|
|
/// Profile name.
|
|
pub(crate) profile_name: std::string::String,
|
|
/// Profile wallet public key.
|
|
pub(crate) wallet_public_key: std::string::String,
|
|
/// Target Token Program ID.
|
|
pub(crate) token_program_id: std::string::String,
|
|
/// Derived ATA from the exact plan.
|
|
pub(crate) associated_token_account: std::string::String,
|
|
/// Wallet balance as lossless JSON text.
|
|
pub(crate) balance_lamports: std::string::String,
|
|
/// Fee estimate as lossless JSON text.
|
|
pub(crate) fee_lamports: std::option::Option<std::string::String>,
|
|
/// Aggregate stateful readiness.
|
|
pub(crate) readiness_status: std::string::String,
|
|
/// Exact simulation outcome.
|
|
pub(crate) simulation_success: bool,
|
|
/// Runtime simulation error.
|
|
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
|
/// Submitted signature.
|
|
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
|
/// Terminal confirmation status.
|
|
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
|
/// Number of ATA-owned materialized facts.
|
|
pub(crate) materialization_count: u32,
|
|
/// Whether the second replay created no output.
|
|
pub(crate) idempotence_validated: bool,
|
|
/// Exact plan JSON.
|
|
pub(crate) plan_json: std::string::String,
|
|
/// Stateful readiness JSON.
|
|
pub(crate) readiness_json: std::string::String,
|
|
/// Simulation JSON.
|
|
pub(crate) simulation_json: std::string::String,
|
|
/// Confirmation and replay diagnostics JSON.
|
|
pub(crate) diagnostics_json: std::string::String,
|
|
}
|
|
|
|
/// Bounded ATA lifecycle journal request.
|
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRequest.ts"
|
|
)]
|
|
pub(crate) struct DemoSplAtaJournalRequest {
|
|
/// Selected Devnet profile.
|
|
pub(crate) profile_name: std::string::String,
|
|
/// Optional partial signature.
|
|
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
|
/// Optional exact mint.
|
|
pub(crate) mint: std::option::Option<std::string::String>,
|
|
/// Optional exact ATA.
|
|
pub(crate) associated_token_account: std::option::Option<std::string::String>,
|
|
/// Optional exact materialized operation.
|
|
pub(crate) operation: std::option::Option<std::string::String>,
|
|
/// Maximum returned rows.
|
|
pub(crate) limit: u32,
|
|
}
|
|
|
|
/// One UI-safe ATA lifecycle journal row.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_ata/DemoSplAtaJournalRow.ts"
|
|
)]
|
|
pub(crate) struct DemoSplAtaJournalRow {
|
|
/// Transaction signature.
|
|
pub(crate) signature: std::string::String,
|
|
/// Slot as lossless JSON text.
|
|
pub(crate) slot: std::string::String,
|
|
/// Materialized family.
|
|
pub(crate) family: std::string::String,
|
|
/// Lifecycle operation.
|
|
pub(crate) operation: std::option::Option<std::string::String>,
|
|
/// Exact mint when present.
|
|
pub(crate) mint: std::option::Option<std::string::String>,
|
|
/// Exact ATA when present.
|
|
pub(crate) associated_token_account: std::option::Option<std::string::String>,
|
|
/// Full bounded payload JSON.
|
|
pub(crate) payload_json: std::string::String,
|
|
}
|
|
|
|
/// Derives the representative profile wallet ATA without exposing private key material.
|
|
pub(crate) async fn demo_spl_ata_derive(
|
|
state: tauri::State<'_, crate::AppState>,
|
|
request: crate::DemoSplAtaDerivationRequest,
|
|
) -> std::result::Result<crate::DemoSplAtaDerivationPayload, std::string::String> {
|
|
let profile =
|
|
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let wallet = match load_profile_wallet(&profile).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let wallet_owner = wallet.public_key();
|
|
let token_program = match parse_token_program(&request.token_program) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let associated_token_account =
|
|
match derive_ata(wallet_owner.as_str(), request.mint.trim(), token_program.program_id()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
return std::result::Result::Ok(crate::DemoSplAtaDerivationPayload {
|
|
payer: wallet_owner.clone(),
|
|
wallet_owner,
|
|
token_program_id: token_program.program_id().to_string(),
|
|
associated_token_account,
|
|
});
|
|
}
|
|
|
|
/// Executes one representative Create or CreateIdempotent ATA flow.
|
|
pub(crate) async fn demo_execution_spl_ata_execute(
|
|
app_handle: tauri::AppHandle,
|
|
state: tauri::State<'_, crate::AppState>,
|
|
request: crate::DemoExecutionSplAtaRequest,
|
|
) -> std::result::Result<crate::DemoExecutionSplAtaSummaryPayload, std::string::String> {
|
|
let acquired = state.demo_execution_solana_core_running().compare_exchange(
|
|
false,
|
|
true,
|
|
std::sync::atomic::Ordering::AcqRel,
|
|
std::sync::atomic::Ordering::Acquire,
|
|
);
|
|
if acquired.is_err() {
|
|
return std::result::Result::Err("a Devnet execution is already running".to_string());
|
|
}
|
|
let _run_guard = crate::DemoExecutionRunGuard {
|
|
running: state.demo_execution_solana_core_running(),
|
|
};
|
|
state
|
|
.demo_execution_solana_core_cancel_requested()
|
|
.store(false, std::sync::atomic::Ordering::Release);
|
|
let profile =
|
|
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let profile_wallet = match load_profile_wallet(&profile).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if request.wallet_owner.trim() != profile_wallet.public_key() {
|
|
return std::result::Result::Err(
|
|
"the representative ATA panel requires the profile wallet as wallet owner".to_string(),
|
|
);
|
|
}
|
|
let token_program = match parse_token_program(&request.token_program) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let wallet_owner = ks_lib::MdPubkey(request.wallet_owner.trim().to_string());
|
|
let mint = ks_lib::MdPubkey(request.mint.trim().to_string());
|
|
let operation = match request.mode.as_str() {
|
|
"create" => ks_lib::ExSplAssociatedTokenAccountOperation::Create {
|
|
wallet_owner,
|
|
mint,
|
|
token_program,
|
|
},
|
|
"create_idempotent" => ks_lib::ExSplAssociatedTokenAccountOperation::CreateIdempotent {
|
|
wallet_owner,
|
|
mint,
|
|
token_program,
|
|
},
|
|
_ => {
|
|
return std::result::Result::Err(
|
|
"ATA mode must be create or create_idempotent".to_string(),
|
|
);
|
|
},
|
|
};
|
|
let mut pipeline_request =
|
|
ks_pipeline_demo_scenarios::DevnetSplAssociatedTokenAccountExecutionRequest::new(
|
|
format!("demo-spl-ata-execution-{}", chrono::Utc::now().timestamp_micros()),
|
|
operation,
|
|
);
|
|
pipeline_request.submit = request.submit;
|
|
pipeline_request.operator_confirmed = request.operator_confirmed;
|
|
pipeline_request.post_validation_max_retries = 20;
|
|
pipeline_request.force_post_validation_replay = request.force_post_validation_replay;
|
|
let http_pool = match ks_onchain_transport::HttpEndpointPool::from_profile(&profile) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
let store = match crate::connect_postgres_store(&profile).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
|
return std::result::Result::Err(error.to_string());
|
|
}
|
|
let decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> = std::vec![
|
|
std::sync::Arc::new(ks_lib::DcSplAssociatedTokenAccountDecoder),
|
|
std::sync::Arc::new(ks_lib::DcSplTokenDecoder),
|
|
];
|
|
let materializers: std::vec::Vec<std::sync::Arc<dyn ks_lib::MtApiEventMaterializer>> = std::vec![
|
|
std::sync::Arc::new(ks_lib::MtTokenAccountsMaterializer),
|
|
std::sync::Arc::new(ks_lib::MtRiskMaterializer),
|
|
];
|
|
let observer = crate::DemoExecutionSolanaCoreObserver {
|
|
app_handle,
|
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
|
};
|
|
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_associated_token_account(
|
|
&http_pool,
|
|
&store,
|
|
&profile,
|
|
crate::workspace_root_dir().as_path(),
|
|
&pipeline_request,
|
|
decoders.as_slice(),
|
|
materializers.as_slice(),
|
|
&observer,
|
|
)
|
|
.await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
return std::result::Result::Ok(summary_payload(summary));
|
|
}
|
|
|
|
/// Loads a bounded journal of ATA-owned lifecycle facts.
|
|
pub(crate) async fn demo_spl_ata_journal(
|
|
state: tauri::State<'_, crate::AppState>,
|
|
request: crate::DemoSplAtaJournalRequest,
|
|
) -> std::result::Result<std::vec::Vec<crate::DemoSplAtaJournalRow>, std::string::String> {
|
|
if request.limit == 0 || request.limit > 500 {
|
|
return std::result::Result::Err("ATA journal limit must be between 1 and 500".to_string());
|
|
}
|
|
let profile =
|
|
match crate::select_devnet_profile(state.app_config(), request.profile_name.as_str()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let store = match crate::connect_postgres_store(&profile).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
let filter = match ks_store::MaterializedEventFilter::new(
|
|
std::option::Option::Some("materializer.token.accounts".to_string()),
|
|
std::option::Option::None,
|
|
request.signature_contains.clone(),
|
|
500,
|
|
) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
let rows = match ks_store::DecodePipelineStore::list_materialized_events(&store, &filter).await
|
|
{
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
let mut output = std::vec::Vec::new();
|
|
for row in rows {
|
|
if row.source_decoder_name != "spl.associated_token_account"
|
|
|| !journal_matches(&row.payload_json, &request)
|
|
{
|
|
continue;
|
|
}
|
|
output.push(journal_row(row));
|
|
if output.len() >= request.limit as usize {
|
|
break;
|
|
}
|
|
}
|
|
return std::result::Result::Ok(output);
|
|
}
|
|
|
|
async fn load_profile_wallet(
|
|
profile: &ks_config::ProfileConfig,
|
|
) -> std::result::Result<ks_wallet::TemporaryWallet, std::string::String> {
|
|
let configured = std::path::PathBuf::from(profile.wallet.wallet_dir.as_str());
|
|
let directory = if configured.is_absolute() {
|
|
configured
|
|
} else {
|
|
crate::workspace_root_dir().join(configured)
|
|
};
|
|
let store = match ks_wallet::TemporaryWalletStore::new(directory) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
let alias = match ks_wallet::WalletAlias::parse(profile.wallet.temporary_wallet_alias.clone()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
|
};
|
|
return match store.load_or_create(alias).await {
|
|
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
|
std::result::Result::Err(error) => std::result::Result::Err(error.to_string()),
|
|
};
|
|
}
|
|
|
|
fn parse_token_program(
|
|
value: &str,
|
|
) -> std::result::Result<ks_lib::ExSplAssociatedTokenProgram, std::string::String> {
|
|
return match value.trim() {
|
|
"classic" => std::result::Result::Ok(ks_lib::ExSplAssociatedTokenProgram::Classic),
|
|
"token_2022" => std::result::Result::Ok(ks_lib::ExSplAssociatedTokenProgram::Token2022),
|
|
_ => std::result::Result::Err("Token Program must be classic or token_2022".to_string()),
|
|
};
|
|
}
|
|
|
|
fn derive_ata(
|
|
wallet: &str,
|
|
mint: &str,
|
|
token_program: &str,
|
|
) -> std::result::Result<std::string::String, std::string::String> {
|
|
let wallet: solana_pubkey::Pubkey = match wallet.parse() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(format!("invalid wallet: {error}"));
|
|
},
|
|
};
|
|
let mint: solana_pubkey::Pubkey = match mint.parse() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(format!("invalid mint: {error}"));
|
|
},
|
|
};
|
|
let token_program: solana_pubkey::Pubkey = match token_program.parse() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => {
|
|
return std::result::Result::Err(format!("invalid Token Program: {error}"));
|
|
},
|
|
};
|
|
return std::result::Result::Ok(
|
|
spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(
|
|
&wallet,
|
|
&mint,
|
|
&token_program,
|
|
)
|
|
.to_string(),
|
|
);
|
|
}
|
|
|
|
fn summary_payload(
|
|
summary: ks_pipeline_demo_scenarios::DevnetSplAssociatedTokenAccountExecutionSummary,
|
|
) -> crate::DemoExecutionSplAtaSummaryPayload {
|
|
let (associated_token_account, token_program_id) = match summary.plan.instructions.first() {
|
|
std::option::Option::Some(instruction) => {
|
|
let ata = match instruction.accounts.get(1) {
|
|
std::option::Option::Some(account) => account.pubkey.0.clone(),
|
|
std::option::Option::None => std::string::String::new(),
|
|
};
|
|
let token_program = match instruction.accounts.last() {
|
|
std::option::Option::Some(account) => account.pubkey.0.clone(),
|
|
std::option::Option::None => std::string::String::new(),
|
|
};
|
|
(ata, token_program)
|
|
},
|
|
std::option::Option::None => (std::string::String::new(), std::string::String::new()),
|
|
};
|
|
let transaction_signature =
|
|
summary.send_result.as_ref().map(|value| return value.signature.0.clone());
|
|
let confirmation_status = summary.confirmation.as_ref().map(|value| {
|
|
return crate::confirmation_status_code(value.status);
|
|
});
|
|
let idempotence_validated = summary.idempotence_replay.as_ref().is_some_and(|replay| {
|
|
return replay.failed_inputs == 0
|
|
&& replay.processing_error_inputs == 0
|
|
&& replay.processors.iter().all(|processor| {
|
|
return processor.failed == 0
|
|
&& processor.processing_errors == 0
|
|
&& processor.materialized_outputs == 0
|
|
&& processor.materialization_refused == 0;
|
|
});
|
|
});
|
|
let materialization_count = match u32::try_from(summary.materializations.len()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => u32::MAX,
|
|
};
|
|
let diagnostics = serde_json::json!({
|
|
"confirmation": summary.confirmation.as_ref(),
|
|
"postStateValidation": summary.post_state_validation.as_ref(),
|
|
"postExecution": summary.post_execution.as_ref(),
|
|
"backfill": summary.backfill.as_ref().map(|value| {
|
|
return serde_json::json!({
|
|
"candidatesCompleted": value.candidates_completed,
|
|
"candidatesCancelled": value.candidates_cancelled,
|
|
"candidatesNotStarted": value.candidates_not_started,
|
|
"canonicalInserted": value.canonical_inserted,
|
|
"canonicalSkipped": value.canonical_skipped,
|
|
"existingSkipped": value.existing_skipped,
|
|
"missing": value.missing,
|
|
"failed": value.failed
|
|
});
|
|
}),
|
|
"coreExtraction": summary.core_extraction.as_ref().map(|value| {
|
|
return serde_json::json!({
|
|
"selected": value.selected,
|
|
"extracted": value.extracted,
|
|
"skipped": value.skipped,
|
|
"failed": value.failed,
|
|
"cancelled": value.cancelled
|
|
});
|
|
}),
|
|
"decodeReplay": summary.decode_replay.as_ref().map(|value| {
|
|
return serde_json::json!({
|
|
"campaignId": value.campaign_id,
|
|
"selected": value.selected,
|
|
"completed": value.completed,
|
|
"unmatched": value.unmatched,
|
|
"failedInputs": value.failed_inputs,
|
|
"processingErrorInputs": value.processing_error_inputs,
|
|
"cancelled": value.cancelled
|
|
});
|
|
}),
|
|
"idempotenceReplay": summary.idempotence_replay.as_ref().map(|value| {
|
|
return serde_json::json!({
|
|
"campaignId": value.campaign_id,
|
|
"selected": value.selected,
|
|
"completed": value.completed,
|
|
"unmatched": value.unmatched,
|
|
"failedInputs": value.failed_inputs,
|
|
"processingErrorInputs": value.processing_error_inputs,
|
|
"cancelled": value.cancelled,
|
|
"materializedOutputs": value.processors.iter().map(|processor| {
|
|
return processor.materialized_outputs;
|
|
}).sum::<u64>(),
|
|
"materializationRefused": value.processors.iter().map(|processor| {
|
|
return processor.materialization_refused;
|
|
}).sum::<u64>()
|
|
});
|
|
}),
|
|
"materializations": summary.materializations.iter().map(|value| {
|
|
return serde_json::json!({
|
|
"processorName": value.processor_name,
|
|
"outputKey": value.output_key,
|
|
"family": value.materialized_family,
|
|
"signature": value.signature,
|
|
"slot": value.slot.to_string(),
|
|
"payload": value.payload_json
|
|
});
|
|
}).collect::<std::vec::Vec<_>>()
|
|
});
|
|
let plan_json = crate::pretty_json(&summary.plan);
|
|
let readiness_json = crate::pretty_json(&summary.stateful_readiness);
|
|
let simulation_json = crate::pretty_json(&summary.simulation);
|
|
let diagnostics_json = crate::pretty_json(&diagnostics);
|
|
return crate::DemoExecutionSplAtaSummaryPayload {
|
|
operation: summary.stateful_readiness.operation_code,
|
|
profile_name: summary.profile_name,
|
|
wallet_public_key: summary.wallet.public_key,
|
|
token_program_id,
|
|
associated_token_account,
|
|
balance_lamports: summary.balance_lamports.to_string(),
|
|
fee_lamports: summary.fee.fee_lamports.map(|value| return value.to_string()),
|
|
readiness_status: match summary.stateful_readiness.status {
|
|
ks_pipeline::SplAssociatedTokenAccountStatefulReadinessStatus::Ready => {
|
|
"ready".to_string()
|
|
},
|
|
ks_pipeline::SplAssociatedTokenAccountStatefulReadinessStatus::Blocked => {
|
|
"blocked".to_string()
|
|
},
|
|
},
|
|
simulation_success: summary.simulation.success,
|
|
simulation_error: summary.simulation.error,
|
|
transaction_signature,
|
|
confirmation_status,
|
|
materialization_count,
|
|
idempotence_validated,
|
|
plan_json,
|
|
readiness_json,
|
|
simulation_json,
|
|
diagnostics_json,
|
|
};
|
|
}
|
|
|
|
fn journal_matches(payload: &serde_json::Value, request: &crate::DemoSplAtaJournalRequest) -> bool {
|
|
for (expected, key) in [
|
|
(&request.operation, "operation"),
|
|
(&request.mint, "mint"),
|
|
(&request.associated_token_account, "associatedTokenAccount"),
|
|
] {
|
|
if expected.as_ref().is_some_and(|value| {
|
|
return payload.get(key).and_then(serde_json::Value::as_str)
|
|
!= std::option::Option::Some(value.as_str());
|
|
}) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn journal_row(row: ks_store::MaterializedEventQueryRow) -> crate::DemoSplAtaJournalRow {
|
|
let text = |key: &str| {
|
|
return row
|
|
.payload_json
|
|
.get(key)
|
|
.and_then(serde_json::Value::as_str)
|
|
.map(std::string::ToString::to_string);
|
|
};
|
|
let operation = text("operation");
|
|
let mint = text("mint");
|
|
let associated_token_account = text("associatedTokenAccount");
|
|
let payload_json = crate::pretty_json(&row.payload_json);
|
|
return crate::DemoSplAtaJournalRow {
|
|
signature: row.signature,
|
|
slot: row.slot.to_string(),
|
|
family: row.materialized_family,
|
|
operation,
|
|
mint,
|
|
associated_token_account,
|
|
payload_json,
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn classic_and_token_2022_derivations_are_distinct_and_canonical() {
|
|
let wallet = ks_program_ids::SYSTEM_PROGRAM_ID;
|
|
let mint = "So11111111111111111111111111111111111111112";
|
|
let classic = super::derive_ata(wallet, mint, ks_program_ids::SPL_TOKEN_PROGRAM_ID)
|
|
.unwrap_or_else(|error| panic!("classic derivation failed: {error}"));
|
|
let token_2022 = super::derive_ata(wallet, mint, ks_program_ids::SPL_TOKEN_2022_PROGRAM_ID)
|
|
.unwrap_or_else(|error| panic!("Token-2022 derivation failed: {error}"));
|
|
assert_eq!(classic, "aqxoAhCwpy3oB1BpNw9hL1HdLYLgPpbPjzxDrrQj3Fs");
|
|
assert_eq!(token_2022, "2sZUUBGq1i6aE47ZoxCaCW89jmYm2EXLPPmNMgMDXHMS");
|
|
}
|
|
|
|
#[test]
|
|
fn lifecycle_journal_filters_are_exact() {
|
|
let request = crate::DemoSplAtaJournalRequest {
|
|
profile_name: "local_devnet".to_string(),
|
|
signature_contains: std::option::Option::None,
|
|
mint: std::option::Option::Some("mint111".to_string()),
|
|
associated_token_account: std::option::Option::Some("ata111".to_string()),
|
|
operation: std::option::Option::Some("create_idempotent".to_string()),
|
|
limit: 100,
|
|
};
|
|
assert!(super::journal_matches(
|
|
&serde_json::json!({
|
|
"operation": "create_idempotent",
|
|
"mint": "mint111",
|
|
"associatedTokenAccount": "ata111"
|
|
}),
|
|
&request,
|
|
));
|
|
assert!(!super::journal_matches(
|
|
&serde_json::json!({
|
|
"operation": "create",
|
|
"mint": "mint111",
|
|
"associatedTokenAccount": "ata111"
|
|
}),
|
|
&request,
|
|
));
|
|
}
|
|
}
|