641 lines
27 KiB
Rust
641 lines
27 KiB
Rust
// file: kb-app-demo-desktop/src/demo_spl_token.rs
|
|
// version: 10
|
|
|
|
//! Thin Tauri adapter for classic SPL Token execution and materialized journals.
|
|
|
|
use ts_rs::TS; // rust-rules: derive-import
|
|
|
|
/// Request for one checked classic SPL Token transfer 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_token/DemoExecutionSplTokenRequest.ts"
|
|
)]
|
|
pub(crate) struct DemoExecutionSplTokenRequest {
|
|
/// Selected Devnet profile.
|
|
pub(crate) profile_name: std::string::String,
|
|
/// Source token account.
|
|
pub(crate) source: std::string::String,
|
|
/// Exact mint account carried by `TransferChecked`.
|
|
pub(crate) mint: std::string::String,
|
|
/// Destination token account.
|
|
pub(crate) destination: std::string::String,
|
|
/// Simple authority resolved by the selected profile wallet.
|
|
pub(crate) authority: std::string::String,
|
|
/// Exact raw amount represented as a decimal string.
|
|
pub(crate) amount_raw: std::string::String,
|
|
/// Expected mint decimals carried by the wire.
|
|
pub(crate) decimals: u8,
|
|
/// Whether the transaction should be signed and submitted after simulation.
|
|
pub(crate) submit: bool,
|
|
/// Explicit operator confirmation for signed submission.
|
|
pub(crate) operator_confirmed: bool,
|
|
/// Whether post-execution core and decode outputs should be force-replayed.
|
|
pub(crate) force_post_validation_replay: bool,
|
|
}
|
|
|
|
/// UI-safe result of one checked SPL Token transfer orchestration.
|
|
#[derive(Clone, Debug, serde::Serialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token/DemoExecutionSplTokenSummaryPayload.ts"
|
|
)]
|
|
pub(crate) struct DemoExecutionSplTokenSummaryPayload {
|
|
/// Stable operation code.
|
|
pub(crate) operation: std::string::String,
|
|
/// Profile used by the orchestration.
|
|
pub(crate) profile_name: std::string::String,
|
|
/// Classified cluster code.
|
|
pub(crate) cluster: std::string::String,
|
|
/// Genesis hash returned by the endpoint.
|
|
pub(crate) genesis_hash: std::string::String,
|
|
/// Source wallet public key.
|
|
pub(crate) wallet_public_key: std::string::String,
|
|
/// Exact wallet balance rendered without JSON integer precision loss.
|
|
pub(crate) balance_lamports: std::string::String,
|
|
/// Estimated fee rendered without JSON integer precision loss.
|
|
pub(crate) fee_lamports: std::option::Option<std::string::String>,
|
|
/// Exact raw token amount.
|
|
pub(crate) amount_raw: std::string::String,
|
|
/// Expected mint decimals.
|
|
pub(crate) decimals: u8,
|
|
/// Aggregate stateful preflight status.
|
|
pub(crate) readiness_status: std::string::String,
|
|
/// Whether exact simulation succeeded.
|
|
pub(crate) simulation_success: bool,
|
|
/// Runtime simulation error when present.
|
|
pub(crate) simulation_error: std::option::Option<std::string::String>,
|
|
/// Submitted transaction signature.
|
|
pub(crate) transaction_signature: std::option::Option<std::string::String>,
|
|
/// Terminal confirmation status.
|
|
pub(crate) confirmation_status: std::option::Option<std::string::String>,
|
|
/// Whether canonical hydration was validated.
|
|
pub(crate) canonical_inserted: bool,
|
|
/// Whether core extraction was validated.
|
|
pub(crate) core_extracted: bool,
|
|
/// Whether SPL Token decode replay was validated.
|
|
pub(crate) decode_replayed: bool,
|
|
/// Number of materialized rows for the submitted instruction.
|
|
pub(crate) materialization_count: u32,
|
|
/// Whether the second replay produced no failure or new output.
|
|
pub(crate) idempotence_validated: bool,
|
|
/// Pretty JSON representation of the exact execution plan.
|
|
pub(crate) plan_json: std::string::String,
|
|
/// Pretty JSON representation of stateful readiness.
|
|
pub(crate) readiness_json: std::string::String,
|
|
/// Pretty JSON representation of the exact simulation result.
|
|
pub(crate) simulation_json: std::string::String,
|
|
/// Pretty JSON representation of confirmation and replay diagnostics.
|
|
pub(crate) diagnostics_json: std::string::String,
|
|
}
|
|
|
|
/// Bounded exact filters for the classic SPL Token materialized journal.
|
|
#[derive(Clone, Debug, serde::Deserialize, TS)]
|
|
#[serde(rename_all = "camelCase")]
|
|
#[ts(
|
|
export,
|
|
export_to = "../frontend/ts/bindings/kb_app_demo_desktop/demo_spl_token/DemoSplTokenJournalRequest.ts"
|
|
)]
|
|
pub(crate) struct DemoSplTokenJournalRequest {
|
|
/// Devnet profile whose PostgreSQL store is queried.
|
|
pub(crate) profile_name: std::string::String,
|
|
/// Optional partial signature handled by the bounded store query.
|
|
pub(crate) signature_contains: std::option::Option<std::string::String>,
|
|
/// Optional exact mint account.
|
|
pub(crate) mint: std::option::Option<std::string::String>,
|
|
/// Optional exact account occurring in the ordered account list.
|
|
pub(crate) account: std::option::Option<std::string::String>,
|
|
/// Optional exact materialized family.
|
|
pub(crate) family: std::option::Option<std::string::String>,
|
|
/// Optional exact operation code without the `spl.token.` prefix.
|
|
pub(crate) operation: std::option::Option<std::string::String>,
|
|
/// Maximum number of rows returned after typed filtering.
|
|
pub(crate) limit: u32,
|
|
}
|
|
|
|
/// UI-safe materialized classic SPL Token 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_token/DemoSplTokenJournalRow.ts"
|
|
)]
|
|
pub(crate) struct DemoSplTokenJournalRow {
|
|
/// Materializer processor name.
|
|
pub(crate) processor_name: std::string::String,
|
|
/// Materializer processor version.
|
|
pub(crate) processor_version: std::string::String,
|
|
/// Stable processor-owned output key.
|
|
pub(crate) output_key: std::string::String,
|
|
/// Transaction signature.
|
|
pub(crate) signature: std::string::String,
|
|
/// Decimal slot rendered as text to preserve JSON precision.
|
|
pub(crate) slot: std::string::String,
|
|
/// Materialized family code.
|
|
pub(crate) family: std::string::String,
|
|
/// Exact operation when present.
|
|
pub(crate) operation: std::option::Option<std::string::String>,
|
|
/// Exact mint account when explicitly available.
|
|
pub(crate) mint: std::option::Option<std::string::String>,
|
|
/// Exact raw amount when carried by the materialized fact.
|
|
pub(crate) amount_raw: std::option::Option<std::string::String>,
|
|
/// Stable outer or inner instruction path when present.
|
|
pub(crate) instruction_path: std::option::Option<std::string::String>,
|
|
/// Ordered account keys preserved by the decoder.
|
|
pub(crate) account_keys: std::vec::Vec<std::string::String>,
|
|
/// Complete bounded typed payload rendered as JSON text.
|
|
pub(crate) payload_json: std::string::String,
|
|
/// Database creation timestamp.
|
|
pub(crate) created_at: std::string::String,
|
|
/// Database replacement timestamp.
|
|
pub(crate) updated_at: std::string::String,
|
|
}
|
|
|
|
/// Executes one checked SPL Token simulation or explicitly authorized Devnet submission.
|
|
pub(crate) async fn demo_execution_spl_token_execute(
|
|
app_handle: tauri::AppHandle,
|
|
state: tauri::State<'_, crate::AppState>,
|
|
request: crate::DemoExecutionSplTokenRequest,
|
|
) -> std::result::Result<crate::DemoExecutionSplTokenSummaryPayload, std::string::String> {
|
|
let acquire_result = state.demo_execution_solana_core_running().compare_exchange(
|
|
false,
|
|
true,
|
|
std::sync::atomic::Ordering::AcqRel,
|
|
std::sync::atomic::Ordering::Acquire,
|
|
);
|
|
if acquire_result.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 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 amount_raw = request.amount_raw.trim().to_string();
|
|
let operation = ks_lib::ExSplClassicTokenOperation::Instruction {
|
|
value: ks_lib::ExSplClassicTokenSingleOperation::TransferChecked {
|
|
source: ks_lib::MdPubkey(request.source.trim().to_string()),
|
|
mint: ks_lib::MdPubkey(request.mint.trim().to_string()),
|
|
destination: ks_lib::MdPubkey(request.destination.trim().to_string()),
|
|
authority: ks_lib::ExSplClassicTokenAuthority {
|
|
authority: ks_lib::MdPubkey(request.authority.trim().to_string()),
|
|
multisig_signers: std::vec::Vec::new(),
|
|
},
|
|
amount: ks_lib::ExSplClassicTokenAmount(amount_raw.clone()),
|
|
decimals: request.decimals,
|
|
},
|
|
};
|
|
let mut pipeline_request = ks_pipeline_demo_scenarios::DevnetSplTokenExecutionRequest::new(
|
|
format!("demo-spl-token-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 decoders: std::vec::Vec<std::sync::Arc<dyn ks_lib::DcApiInstructionDecoder>> =
|
|
std::vec![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::MtAdminMaterializer),
|
|
std::sync::Arc::new(ks_lib::MtRiskMaterializer),
|
|
];
|
|
let observer = crate::DemoExecutionSolanaCoreObserver {
|
|
app_handle,
|
|
cancel_requested: state.demo_execution_solana_core_cancel_requested(),
|
|
};
|
|
let workspace_root = crate::workspace_root_dir();
|
|
let summary = match ks_pipeline_demo_scenarios::execute_devnet_spl_token(
|
|
&http_pool,
|
|
&store,
|
|
&profile,
|
|
workspace_root.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, amount_raw, request.decimals));
|
|
}
|
|
|
|
/// Loads a bounded, typed journal of committed classic SPL Token projections.
|
|
pub(crate) async fn demo_spl_token_journal(
|
|
state: tauri::State<'_, crate::AppState>,
|
|
request: crate::DemoSplTokenJournalRequest,
|
|
) -> std::result::Result<std::vec::Vec<crate::DemoSplTokenJournalRow>, std::string::String> {
|
|
let validated = match validate_journal_request(request) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let profile =
|
|
match crate::select_devnet_profile(state.app_config(), validated.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()),
|
|
};
|
|
if let std::result::Result::Err(error) = store.initialize_store_schema().await {
|
|
return std::result::Result::Err(error.to_string());
|
|
}
|
|
let needs_payload_filter =
|
|
validated.mint.is_some() || validated.account.is_some() || validated.operation.is_some();
|
|
let query_limit = if needs_payload_filter {
|
|
ks_store::MAX_MATERIALIZED_EVENT_QUERY_ROWS
|
|
} else {
|
|
validated.limit
|
|
};
|
|
let filter = match ks_store::MaterializedEventFilter::new(
|
|
std::option::Option::None,
|
|
validated.family.clone(),
|
|
validated.signature_contains.clone(),
|
|
query_limit,
|
|
) {
|
|
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.token" || !payload_matches(&row.payload_json, &validated)
|
|
{
|
|
continue;
|
|
}
|
|
output.push(journal_row(row));
|
|
if output.len() >= validated.limit as usize {
|
|
break;
|
|
}
|
|
}
|
|
return std::result::Result::Ok(output);
|
|
}
|
|
|
|
fn summary_payload(
|
|
summary: ks_pipeline_demo_scenarios::DevnetSplTokenExecutionSummary,
|
|
amount_raw: std::string::String,
|
|
decimals: u8,
|
|
) -> crate::DemoExecutionSplTokenSummaryPayload {
|
|
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 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 post_execution = summary.post_execution.as_ref();
|
|
let canonical_inserted = post_execution.is_some_and(|value| return value.canonical_inserted);
|
|
let core_extracted = post_execution.is_some_and(|value| return value.core_extracted);
|
|
let decode_replayed = post_execution.is_some_and(|value| return value.decode_replayed);
|
|
let idempotence_validated = summary.idempotence_replay.as_ref().is_some_and(|replay| {
|
|
return replay.failed_inputs == 0
|
|
&& replay.processing_error_inputs == 0
|
|
&& !replay.cancelled
|
|
&& 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,
|
|
"postExecution": summary.post_execution,
|
|
"backfill": summary.backfill.as_ref().map(|value| {
|
|
return serde_json::json!({
|
|
"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 replay_diagnostics(value);
|
|
}),
|
|
"idempotenceReplay": summary.idempotence_replay.as_ref().map(|value| {
|
|
return replay_diagnostics(value);
|
|
}),
|
|
"materializations": summary.materializations
|
|
});
|
|
return crate::DemoExecutionSplTokenSummaryPayload {
|
|
operation: summary.stateful_readiness.operation_code,
|
|
profile_name: summary.profile_name,
|
|
cluster: crate::execution_cluster_code(summary.cluster),
|
|
genesis_hash: summary.genesis_hash,
|
|
wallet_public_key: summary.wallet.public_key,
|
|
balance_lamports: summary.balance_lamports.to_string(),
|
|
fee_lamports: summary.fee.fee_lamports.map(|value| return value.to_string()),
|
|
amount_raw,
|
|
decimals,
|
|
readiness_status: match summary.stateful_readiness.status {
|
|
ks_pipeline::SplTokenStatefulReadinessStatus::Ready => "ready".to_string(),
|
|
ks_pipeline::SplTokenStatefulReadinessStatus::Blocked => "blocked".to_string(),
|
|
},
|
|
simulation_success: summary.simulation.success,
|
|
simulation_error: summary.simulation.error,
|
|
transaction_signature,
|
|
confirmation_status,
|
|
canonical_inserted,
|
|
core_extracted,
|
|
decode_replayed,
|
|
materialization_count,
|
|
idempotence_validated,
|
|
plan_json,
|
|
readiness_json,
|
|
simulation_json,
|
|
diagnostics_json: crate::pretty_json(&diagnostics),
|
|
};
|
|
}
|
|
|
|
fn replay_diagnostics(summary: &ks_pipeline::DecodeReplaySummary) -> serde_json::Value {
|
|
return serde_json::json!({
|
|
"campaignId": summary.campaign_id,
|
|
"selected": summary.selected,
|
|
"completed": summary.completed,
|
|
"failedInputs": summary.failed_inputs,
|
|
"processingErrorInputs": summary.processing_error_inputs,
|
|
"cancelled": summary.cancelled,
|
|
"processors": summary.processors.iter().map(|processor| {
|
|
return serde_json::json!({
|
|
"name": processor.processor_name,
|
|
"version": processor.processor_version,
|
|
"decoded": processor.decoded,
|
|
"skipped": processor.skipped,
|
|
"failed": processor.failed,
|
|
"processingErrors": processor.processing_errors,
|
|
"materializedOutputs": processor.materialized_outputs,
|
|
"materializationRefused": processor.materialization_refused
|
|
});
|
|
}).collect::<std::vec::Vec<_>>()
|
|
});
|
|
}
|
|
|
|
fn validate_journal_request(
|
|
request: crate::DemoSplTokenJournalRequest,
|
|
) -> std::result::Result<crate::DemoSplTokenJournalRequest, std::string::String> {
|
|
if request.limit == 0 || request.limit > ks_store::MAX_MATERIALIZED_EVENT_QUERY_ROWS {
|
|
return std::result::Result::Err(format!(
|
|
"journal limit must be between 1 and {}",
|
|
ks_store::MAX_MATERIALIZED_EVENT_QUERY_ROWS
|
|
));
|
|
}
|
|
if request.profile_name.trim().is_empty() {
|
|
return std::result::Result::Err("journal profile name must not be empty".to_string());
|
|
}
|
|
let signature_contains =
|
|
match bounded_optional(request.signature_contains, "signature filter", 128) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let mint = match bounded_optional(request.mint, "mint filter", 64) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let account = match bounded_optional(request.account, "account filter", 64) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let operation = match bounded_optional(request.operation, "operation filter", 64) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let family = match bounded_optional(request.family, "family filter", 32) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if family.as_ref().is_some_and(|value| {
|
|
return !matches!(value.as_str(), "token_account" | "admin" | "risk");
|
|
}) {
|
|
return std::result::Result::Err(
|
|
"journal family must be token_account, admin or risk".to_string(),
|
|
);
|
|
}
|
|
return std::result::Result::Ok(crate::DemoSplTokenJournalRequest {
|
|
profile_name: request.profile_name.trim().to_string(),
|
|
signature_contains,
|
|
mint,
|
|
account,
|
|
family,
|
|
operation,
|
|
limit: request.limit,
|
|
});
|
|
}
|
|
|
|
fn bounded_optional(
|
|
value: std::option::Option<std::string::String>,
|
|
label: &str,
|
|
maximum_length: usize,
|
|
) -> std::result::Result<std::option::Option<std::string::String>, std::string::String> {
|
|
let trimmed = value.map(|text| return text.trim().to_string());
|
|
let trimmed = trimmed.filter(|text| return !text.is_empty());
|
|
if trimmed.as_ref().is_some_and(|text| return text.len() > maximum_length) {
|
|
return std::result::Result::Err(format!("{label} must not exceed {maximum_length} bytes"));
|
|
}
|
|
return std::result::Result::Ok(trimmed);
|
|
}
|
|
|
|
fn payload_matches(
|
|
payload: &serde_json::Value,
|
|
request: &crate::DemoSplTokenJournalRequest,
|
|
) -> bool {
|
|
if request.operation.as_ref().is_some_and(|expected| {
|
|
return payload.get("operation").and_then(serde_json::Value::as_str)
|
|
!= std::option::Option::Some(expected.as_str());
|
|
}) {
|
|
return false;
|
|
}
|
|
if request.mint.as_ref().is_some_and(|expected| {
|
|
return payload_mint(payload).as_deref() != std::option::Option::Some(expected.as_str());
|
|
}) {
|
|
return false;
|
|
}
|
|
if request.account.as_ref().is_some_and(|expected| {
|
|
return !payload_account_keys(payload).iter().any(|value| return value == expected);
|
|
}) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
fn journal_row(row: ks_store::MaterializedEventQueryRow) -> DemoSplTokenJournalRow {
|
|
let operation = row
|
|
.payload_json
|
|
.get("operation")
|
|
.and_then(serde_json::Value::as_str)
|
|
.map(std::string::ToString::to_string);
|
|
let amount_raw = row
|
|
.payload_json
|
|
.get("amountRaw")
|
|
.or_else(|| {
|
|
return row
|
|
.payload_json
|
|
.get("parameters")
|
|
.and_then(|value| return value.get("amountRaw"));
|
|
})
|
|
.and_then(serde_json::Value::as_str)
|
|
.map(std::string::ToString::to_string);
|
|
let instruction_path = row
|
|
.payload_json
|
|
.get("instructionPath")
|
|
.and_then(serde_json::Value::as_str)
|
|
.map(std::string::ToString::to_string);
|
|
let payload_json = crate::pretty_json(&row.payload_json);
|
|
return DemoSplTokenJournalRow {
|
|
processor_name: row.processor_name,
|
|
processor_version: row.processor_version,
|
|
output_key: row.output_key,
|
|
signature: row.signature,
|
|
slot: row.slot.to_string(),
|
|
family: row.materialized_family,
|
|
operation,
|
|
mint: payload_mint(&row.payload_json),
|
|
amount_raw,
|
|
instruction_path,
|
|
account_keys: payload_account_keys(&row.payload_json),
|
|
payload_json,
|
|
created_at: row.created_at,
|
|
updated_at: row.updated_at,
|
|
};
|
|
}
|
|
|
|
fn payload_mint(payload: &serde_json::Value) -> std::option::Option<std::string::String> {
|
|
let direct = payload.get("mint").and_then(serde_json::Value::as_str);
|
|
if let std::option::Option::Some(value) = direct {
|
|
return std::option::Option::Some(value.to_string());
|
|
}
|
|
return payload.get("accounts").and_then(serde_json::Value::as_array).and_then(|rows| {
|
|
return rows.iter().find_map(|row| {
|
|
if row.get("role").and_then(serde_json::Value::as_str)
|
|
!= std::option::Option::Some("mint")
|
|
{
|
|
return std::option::Option::None;
|
|
}
|
|
return row
|
|
.get("accountKey")
|
|
.and_then(serde_json::Value::as_str)
|
|
.map(std::string::ToString::to_string);
|
|
});
|
|
});
|
|
}
|
|
|
|
fn payload_account_keys(payload: &serde_json::Value) -> std::vec::Vec<std::string::String> {
|
|
let values = payload.get("accounts").and_then(serde_json::Value::as_array).map(|rows| {
|
|
return rows
|
|
.iter()
|
|
.filter_map(|row| {
|
|
return row
|
|
.get("accountKey")
|
|
.and_then(serde_json::Value::as_str)
|
|
.map(std::string::ToString::to_string);
|
|
})
|
|
.collect();
|
|
});
|
|
return match values {
|
|
std::option::Option::Some(values) => values,
|
|
std::option::Option::None => std::vec::Vec::new(),
|
|
};
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[test]
|
|
fn journal_filters_are_exact_bounded_and_preserve_raw_amounts() {
|
|
let request = crate::DemoSplTokenJournalRequest {
|
|
profile_name: "local_devnet".to_string(),
|
|
signature_contains: std::option::Option::None,
|
|
mint: std::option::Option::Some("mint111".to_string()),
|
|
account: std::option::Option::Some("source111".to_string()),
|
|
family: std::option::Option::Some("token_account".to_string()),
|
|
operation: std::option::Option::Some("transfer_checked".to_string()),
|
|
limit: 25,
|
|
};
|
|
let payload = serde_json::json!({
|
|
"operation": "transfer_checked",
|
|
"mint": "mint111",
|
|
"amountRaw": "18446744073709551615",
|
|
"accounts": [
|
|
{"role": "source", "accountKey": "source111"},
|
|
{"role": "mint", "accountKey": "mint111"}
|
|
]
|
|
});
|
|
assert!(super::payload_matches(&payload, &request));
|
|
assert_eq!(
|
|
payload.get("amountRaw").and_then(serde_json::Value::as_str),
|
|
std::option::Option::Some("18446744073709551615")
|
|
);
|
|
let row = super::journal_row(ks_store::MaterializedEventQueryRow {
|
|
processor_name: "materializer.token.accounts".to_string(),
|
|
processor_version: "0.4.4".to_string(),
|
|
input_key: "input".to_string(),
|
|
output_key: "output".to_string(),
|
|
source_event_key: "event".to_string(),
|
|
source_decoder_name: "spl.token".to_string(),
|
|
source_decoder_version: "0.4.4".to_string(),
|
|
signature: "signature".to_string(),
|
|
slot: u64::MAX,
|
|
materialized_family: "token_account".to_string(),
|
|
payload_json: payload,
|
|
created_at: "created".to_string(),
|
|
updated_at: "updated".to_string(),
|
|
});
|
|
assert_eq!(row.slot, u64::MAX.to_string());
|
|
assert_eq!(row.amount_raw, std::option::Option::Some(u64::MAX.to_string()));
|
|
}
|
|
|
|
#[test]
|
|
fn journal_rejects_unbounded_or_unknown_family_requests() {
|
|
let request = crate::DemoSplTokenJournalRequest {
|
|
profile_name: "local_devnet".to_string(),
|
|
signature_contains: std::option::Option::None,
|
|
mint: std::option::Option::None,
|
|
account: std::option::Option::None,
|
|
family: std::option::Option::Some("fee".to_string()),
|
|
operation: std::option::Option::None,
|
|
limit: 501,
|
|
};
|
|
assert!(super::validate_journal_request(request).is_err());
|
|
}
|
|
}
|