v0.1.0-pre.062

This commit is contained in:
2026-07-30 10:27:06 +02:00
parent f40fb78817
commit 460671e19d
319 changed files with 392513 additions and 9507 deletions

View File

@@ -0,0 +1,152 @@
// file: kb-pipeline-demo-scenarios/src/bin/kb_pipeline_demo_scenarios_cli.rs
// version: 1
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
#![warn(missing_docs)]
//! Command-line entry point for reusable Devnet fixture preparation.
macro_rules! kb_try {
($expression:expr) => {
match $expression {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
};
}
#[tokio::main]
async fn main() {
let result = run().await;
if let std::result::Result::Err(error) = result {
eprintln!("{}: {}", error.code(), error.message());
std::process::exit(1);
}
}
async fn run() -> kb_core::Result<()> {
let arguments = std::env::args().skip(1).collect::<std::vec::Vec<_>>();
let command = match arguments.first() {
std::option::Option::Some(value) => value.as_str(),
std::option::Option::None => return usage_error("missing command"),
};
if command != "prepare-token-2022-fixture" {
return usage_error(format!("unknown command {command}").as_str());
}
let options = kb_try!(parse_prepare_options(arguments.as_slice()));
let summary = kb_try!(kb_pipeline_demo_scenarios::prepare_token_2022_fixture(&options).await);
let output = match serde_json::to_string_pretty(&summary) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::json(format!(
"unable to serialize Token-2022 fixture summary: {error}"
)));
},
};
println!("{output}");
return std::result::Result::Ok(());
}
fn parse_prepare_options(
arguments: &[std::string::String],
) -> kb_core::Result<kb_pipeline_demo_scenarios::Token2022FixturePreparationOptions> {
let mut rpc_url = std::option::Option::None;
let mut wallet_path = std::option::Option::None;
let mut wallet_dir = std::option::Option::None;
let mut decimals = 9_u8;
let mut index = 1_usize;
while index < arguments.len() {
let name = arguments[index].as_str();
let value = match arguments.get(index + 1) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return usage_error(format!("missing value for {name}").as_str());
},
};
match name {
"--rpc-url" => rpc_url = std::option::Option::Some(value.clone()),
"--wallet" => wallet_path = std::option::Option::Some(std::path::PathBuf::from(value)),
"--wallet-dir" => {
wallet_dir = std::option::Option::Some(std::path::PathBuf::from(value))
},
"--decimals" => {
decimals = match value.parse::<u8>() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::config(format!(
"invalid --decimals value {value}: {error}"
)));
},
}
},
_ => return usage_error(format!("unknown option {name}").as_str()),
}
index += 2;
}
let rpc_url = match rpc_url.or_else(|| return std::env::var("KB_DEVNET_RPC_URL").ok()) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::config(
"missing --rpc-url or KB_DEVNET_RPC_URL",
));
},
};
let wallet_path = match wallet_path
.or_else(|| return std::env::var("KB_DEVNET_WALLET").ok().map(std::path::PathBuf::from))
{
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::config(
"missing --wallet or KB_DEVNET_WALLET",
));
},
};
let wallet_dir = match wallet_dir {
std::option::Option::Some(value) => value,
std::option::Option::None => match wallet_path.parent() {
std::option::Option::Some(value) => std::path::Path::to_path_buf(value),
std::option::Option::None => std::path::PathBuf::from("."),
},
};
return std::result::Result::Ok(
kb_pipeline_demo_scenarios::Token2022FixturePreparationOptions {
rpc_url,
wallet_path,
wallet_dir,
decimals,
},
);
}
fn usage_error<T>(message: &str) -> kb_core::Result<T> {
return std::result::Result::Err(kb_core::Error::config(format!(
"{message}. Usage: cargo run -p kb-pipeline-demo-scenarios --bin kb-pipeline-demo-scenarios-cli -- prepare-token-2022-fixture --rpc-url URL --wallet PATH [--wallet-dir PATH] [--decimals 9]"
)));
}
#[cfg(test)]
mod tests {
#[test]
fn prepare_options_accept_explicit_values() {
let arguments = vec![
"prepare-token-2022-fixture".to_string(),
"--rpc-url".to_string(),
"https://api.devnet.solana.com".to_string(),
"--wallet".to_string(),
"wallet.json".to_string(),
"--wallet-dir".to_string(),
"wallet-dir".to_string(),
"--decimals".to_string(),
"9".to_string(),
];
let options = super::parse_prepare_options(arguments.as_slice());
assert!(options.is_ok());
let options = match options {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(options.wallet_dir, std::path::PathBuf::from("wallet-dir"));
assert_eq!(options.decimals, 9);
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/environment.rs
// version: 1
// version: 2
//! Environment initialization for opt-in demonstration scenarios.
@@ -9,3 +9,176 @@ pub fn initialize_demo_scenario_environment(
) -> kb_core::Result<kb_config::EnvironmentLoadReport> {
return kb_config::load_workspace_environment(workspace_root);
}
/// Resolves one compatible Devnet profile for demo scenarios.
///
/// An explicit name has priority, followed by `KB_DEVNET_PROFILE`, then the
/// first compatible Devnet profile declared by the configuration.
pub fn resolve_demo_devnet_profile(
config: &kb_config::AppConfig,
requested_profile_name: std::option::Option<&str>,
) -> kb_core::Result<kb_config::ProfileConfig> {
let explicit = requested_profile_name
.map(str::trim)
.filter(|value| return !value.is_empty())
.map(std::string::ToString::to_string);
let selected_name = match explicit {
std::option::Option::Some(value) => std::option::Option::Some(value),
std::option::Option::None => std::env::var("KB_DEVNET_PROFILE")
.ok()
.map(|value| return value.trim().to_string())
.filter(|value| return !value.is_empty()),
};
for profile in &config.profiles {
let compatible = profile.wallet.cluster == "devnet"
&& profile.wallet.temporary_wallet_enabled
&& profile.wallet.temporary_wallet_persist;
if !compatible {
continue;
}
if let std::option::Option::Some(name) = selected_name.as_ref()
&& profile.name != *name
{
continue;
}
return std::result::Result::Ok(profile.clone());
}
return match selected_name {
std::option::Option::Some(name) => std::result::Result::Err(kb_core::Error::config(
format!("compatible Devnet profile '{name}' is unavailable"),
)),
std::option::Option::None => std::result::Result::Err(kb_core::Error::config(
"no compatible Devnet profile is configured",
)),
};
}
/// Readiness report for one Devnet profile PostgreSQL store.
#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DevnetProfileStoreReadiness {
/// Selected Devnet profile.
pub profile_name: std::string::String,
/// Whether schema creation was allowed by configuration.
pub auto_initialize_schema: bool,
/// Number of known tables already present before preparation.
pub existing_tables_before: usize,
/// Number of known tables created during preparation.
pub created_tables: usize,
/// Number of known tables present after preparation.
pub existing_tables_after: usize,
/// Total number of known tables expected by the current store.
pub expected_tables: usize,
}
/// Verifies or initializes the PostgreSQL schema selected by one Devnet profile.
pub async fn prepare_demo_devnet_profile_store(
profile: &kb_config::ProfileConfig,
) -> kb_core::Result<DevnetProfileStoreReadiness> {
if profile.wallet.cluster != "devnet" {
return std::result::Result::Err(kb_core::Error::config(format!(
"profile '{}' is not configured for Devnet",
profile.name
)));
}
if !profile.database.enabled {
return std::result::Result::Err(kb_core::Error::config(format!(
"Devnet profile '{}' has database storage disabled",
profile.name
)));
}
if profile.database.backend != "postgres" {
return std::result::Result::Err(kb_core::Error::config(format!(
"Devnet profile '{}' must use the PostgreSQL backend",
profile.name
)));
}
let options = match kb_store::PostgresStoreOptions::new(
profile.database.postgres.url.clone(),
profile.database.postgres.max_connections,
profile.database.postgres.connect_timeout_ms,
false,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let store = match kb_store::PostgresStore::connect(options).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let before = match store.known_table_diagnostics().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let existing_tables_before = before.iter().filter(|table| return table.exists).count();
if profile.database.postgres.auto_initialize_schema
&& let std::result::Result::Err(error) = store.initialize_store_schema().await
{
return std::result::Result::Err(error);
}
let after = match store.known_table_diagnostics().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let existing_tables_after = after.iter().filter(|table| return table.exists).count();
if existing_tables_after != after.len() {
let missing = after
.iter()
.filter(|table| return !table.exists)
.map(|table| return table.table_name.clone())
.collect::<std::vec::Vec<_>>();
return std::result::Result::Err(kb_core::Error::config(format!(
"Devnet profile '{}' PostgreSQL schema is incomplete and auto initialization is {}: missing {}",
profile.name,
if profile.database.postgres.auto_initialize_schema {
"enabled"
} else {
"disabled"
},
missing.join(", ")
)));
}
return std::result::Result::Ok(DevnetProfileStoreReadiness {
profile_name: profile.name.clone(),
auto_initialize_schema: profile.database.postgres.auto_initialize_schema,
existing_tables_before,
created_tables: existing_tables_after.saturating_sub(existing_tables_before),
existing_tables_after,
expected_tables: after.len(),
});
}
#[cfg(test)]
mod tests {
fn example_config() -> kb_config::AppConfig {
return 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}"),
};
}
#[test]
fn resolver_finds_a_compatible_devnet_profile_without_a_fixed_name() {
let config = example_config();
let profile = match super::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),
};
assert_eq!(profile.wallet.cluster, "devnet");
assert!(profile.wallet.temporary_wallet_enabled);
assert!(profile.wallet.temporary_wallet_persist);
}
#[test]
fn resolver_rejects_an_explicit_non_devnet_profile() {
let config = example_config();
assert!(
super::resolve_demo_devnet_profile(
&config,
std::option::Option::Some("mainnet_research"),
)
.is_err()
);
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/lib.rs
// version: 3
// version: 5
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -17,9 +17,16 @@ mod solana_token_2022_devnet_scenarios;
mod solana_token_2022_validation;
mod solana_token_execution;
mod solana_token_lifecycle;
mod token_2022_fixture;
/// Readiness report for one Devnet profile PostgreSQL store.
pub use self::environment::DevnetProfileStoreReadiness;
/// Loads the workspace environment for opt-in demo and Devnet scenarios.
pub use self::environment::initialize_demo_scenario_environment;
/// Verifies or initializes the PostgreSQL schema selected by one Devnet profile.
pub use self::environment::prepare_demo_devnet_profile_store;
/// Resolves one configured Devnet profile for UI, CLI and test scenarios.
pub use self::environment::resolve_demo_devnet_profile;
/// Complete request for one Devnet Associated Token Account execution.
pub use self::solana_ata_execution::DevnetSplAssociatedTokenAccountExecutionRequest;
/// Complete result of one Devnet Associated Token Account execution.
@@ -128,6 +135,20 @@ pub use self::solana_token_lifecycle::DevnetSplTokenLifecycleSummary;
pub use self::solana_token_lifecycle::execute_devnet_spl_token_lifecycle;
/// Prepares raw accounts for one controlled Devnet SPL Token lifecycle.
pub use self::solana_token_lifecycle::prepare_devnet_spl_token_lifecycle_accounts;
/// Default raw amount delegated by the public ApproveChecked scenario.
pub use self::token_2022_fixture::DEFAULT_TOKEN_2022_APPROVE_AMOUNT_RAW;
/// Default raw amount burned by the public BurnChecked scenario.
pub use self::token_2022_fixture::DEFAULT_TOKEN_2022_BURN_AMOUNT_RAW;
/// Default raw amount minted by the public MintToChecked scenario.
pub use self::token_2022_fixture::DEFAULT_TOKEN_2022_MINT_AMOUNT_RAW;
/// Default raw amount transferred by the public TransferChecked scenario.
pub use self::token_2022_fixture::DEFAULT_TOKEN_2022_TRANSFER_AMOUNT_RAW;
/// Command-line options for one Token-2022 fixture preparation.
pub use self::token_2022_fixture::Token2022FixturePreparationOptions;
/// Public values written to one Token-2022 fixture file.
pub use self::token_2022_fixture::Token2022FixturePreparationSummary;
/// Creates or reuses one complete Token-2022 public scenario fixture.
pub use self::token_2022_fixture::prepare_token_2022_fixture;
/// Canonical tracing target for demo pipeline scenarios.
pub(crate) use self::constants::TRACING_TARGET;

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_ata_execution.rs
// version: 7
// version: 9
//! Devnet ATA execution with stateful and canonical post-validation.
@@ -406,7 +406,7 @@ where
};
summary.materializations = rows
.into_iter()
.filter(|row| return row.source_decoder_name == "spl_associated_token_account")
.filter(|row| return row.source_decoder_name == "spl.associated_token_account")
.collect();
diagnostic.materialized =
!summary.plan.policy.post_execution_validation.materialization_required
@@ -992,18 +992,16 @@ mod tests {
};
}
fn local_devnet_profile() -> kb_config::ProfileConfig {
fn example_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");
return match crate::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(profile) => profile,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),
};
}
fn create_operation(wallet: &str) -> kb_lib::ExSplAssociatedTokenAccountOperation {
@@ -1029,7 +1027,7 @@ mod tests {
#[test]
fn creation_and_recovery_intents_preserve_exact_spend_and_signers() {
let profile = local_devnet_profile();
let profile = example_devnet_profile();
let fee_payer = kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string());
let create = crate::DevnetSplAssociatedTokenAccountExecutionRequest::new(
"ata-2",
@@ -1090,7 +1088,7 @@ mod tests {
};
let submit = std::env::var("KB_DEVNET_SPL_ATA_SUBMIT").ok().as_deref()
== std::option::Option::Some("1");
let profile = local_devnet_profile();
let profile = example_devnet_profile();
let workspace_root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.map(std::path::Path::to_path_buf)

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_execution.rs
// version: 8
// version: 9
//! Devnet Solana execution orchestration with canonical post-validation.
@@ -1223,18 +1223,16 @@ pub(crate) fn emit<O>(
#[cfg(test)]
mod tests {
fn local_devnet_profile() -> kb_config::ProfileConfig {
fn example_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 from example config");
return match crate::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(profile) => profile,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),
};
}
fn recipient() -> kb_lib::MdPubkey {
@@ -1243,7 +1241,7 @@ mod tests {
#[test]
fn request_and_profile_validation_are_conservative() {
let profile = local_devnet_profile();
let profile = example_devnet_profile();
let mut request = crate::DevnetSystemTransferRequest::new("intent-1", recipient(), 1_000);
assert!(request.validate().is_ok());
assert!(super::validate_devnet_profile(&profile, &request).is_ok());
@@ -1256,8 +1254,8 @@ mod tests {
}
#[test]
fn local_devnet_profile_routes_complete_execution_flow() {
let profile = local_devnet_profile();
fn resolved_devnet_profile_routes_complete_execution_flow() {
let profile = example_devnet_profile();
let pool = match kb_onchain_transport::HttpEndpointPool::from_profile(&profile) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("HTTP pool creation failed: {error}"),
@@ -1284,7 +1282,7 @@ mod tests {
#[test]
fn transfer_plan_uses_exact_devnet_policy() {
let profile = local_devnet_profile();
let profile = example_devnet_profile();
let mut request = crate::DevnetSystemTransferRequest::new("intent-2", recipient(), 5_000);
request.submit = true;
request.operator_confirmed = true;
@@ -1344,7 +1342,7 @@ mod tests {
panic!("KB_POSTGRES_TEST_URL is required: {error}");
},
};
let mut profile = local_devnet_profile();
let mut profile = example_devnet_profile();
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
if let std::result::Result::Ok(directory) = std::env::var("KB_DEVNET_WALLET_DIR") {

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_memo_execution.rs
// version: 3
// version: 5
//! Devnet SPL Memo v4 execution with canonical post-validation.
@@ -458,7 +458,7 @@ where
diagnostic.decode_replayed = decode_completed(&first_replay);
summary.decode_replay = std::option::Option::Some(first_replay);
let filter = match kb_store::MaterializedEventFilter::new(
std::option::Option::Some("transaction_annotations".to_string()),
std::option::Option::Some("materializer.transaction.annotations".to_string()),
std::option::Option::Some("transaction_annotation".to_string()),
std::option::Option::Some(signature.0.clone()),
8,
@@ -753,18 +753,16 @@ fn decode_completed(summary: &kb_pipeline::DecodeReplaySummary) -> bool {
#[cfg(test)]
mod tests {
fn local_devnet_profile() -> kb_config::ProfileConfig {
fn example_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");
return match crate::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(profile) => profile,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),
};
}
#[test]
@@ -779,7 +777,7 @@ mod tests {
#[test]
fn exact_v4_plan_has_zero_spend_and_wallet_signer() {
let profile = local_devnet_profile();
let profile = example_devnet_profile();
let fee_payer = kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string());
let mut request = crate::DevnetMemoExecutionRequest::new("memo-3", "hello");
request.submit = true;
@@ -800,7 +798,7 @@ mod tests {
#[test]
fn submission_requires_profile_enablement_and_confirmation() {
let profile = local_devnet_profile();
let profile = example_devnet_profile();
let mut request = crate::DevnetMemoExecutionRequest::new("memo-4", "hello");
assert!(super::validate_profile(&profile, &request).is_ok());
request.submit = true;
@@ -822,7 +820,7 @@ mod tests {
panic!("KB_POSTGRES_TEST_URL is required: {error}");
},
};
let mut profile = local_devnet_profile();
let mut profile = example_devnet_profile();
profile.database.backend = "postgres".to_string();
profile.database.postgres.url = database_url;
if let std::result::Result::Ok(directory) = std::env::var("KB_DEVNET_WALLET_DIR") {

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_devnet_execution.rs
// version: 4
// version: 5
//! Devnet Token-2022 execution with stateful and canonical post-validation.
@@ -372,7 +372,7 @@ where
};
summary.materializations = rows
.into_iter()
.filter(|row| return row.source_decoder_name == "spl_token_2022")
.filter(|row| return row.source_decoder_name == "spl.token_2022")
.collect();
diagnostic.materialized =
!summary.plan.policy.post_execution_validation.materialization_required

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_2022_devnet_scenarios.rs
// version: 6
// version: 7
//! Stable Devnet validation scenarios required to close milestone 0.4.6.
@@ -61,8 +61,8 @@ pub fn devnet_spl_validation_scenarios() -> std::vec::Vec<crate::DevnetSplValida
public_scenario("token_2022_freeze_account", "Token-2022 FreezeAccount", kb_lib::EX_SPL_TOKEN_2022_FREEZE_ACCOUNT_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_FREEZE_AUTHORITY"]),
public_scenario("token_2022_thaw_account", "Token-2022 ThawAccount", kb_lib::EX_SPL_TOKEN_2022_THAW_ACCOUNT_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "TOKEN_2022_FREEZE_AUTHORITY"]),
public_scenario("token_2022_close_destination", "Token-2022 CloseAccount destination", kb_lib::EX_SPL_TOKEN_2022_CLOSE_ACCOUNT_OPERATION, &["TOKEN_2022_DESTINATION", "KB_DEVNET_WALLET_ADDRESS", "TOKEN_2022_AUTHORITY"]),
registry_scenario("elgamal_registry_create", "ElGamal Registry CreateRegistry", "spl_elgamal_registry.create_registry", &["ELGAMAL_REGISTRY_ADDRESS", "ELGAMAL_PUBKEY_BASE64", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
registry_scenario("elgamal_registry_update", "ElGamal Registry UpdateRegistry", "spl_elgamal_registry.update_registry", &["ELGAMAL_REGISTRY_ADDRESS", "ELGAMAL_PUBKEY_BASE64", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
registry_scenario("elgamal_registry_create", "ElGamal Registry CreateRegistry", "spl.elgamal_registry.create_registry", &["ELGAMAL_REGISTRY_ADDRESS", "ELGAMAL_PUBKEY_BASE64", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
registry_scenario("elgamal_registry_update", "ElGamal Registry UpdateRegistry", "spl.elgamal_registry.update_registry", &["ELGAMAL_REGISTRY_ADDRESS", "ELGAMAL_PUBKEY_BASE64", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
confidential_scenario("token_2022_configure_confidential_account", "Token-2022 ConfigureAccountWithRegistry", kb_lib::EX_SPL_TOKEN_2022_CONFIGURE_CONFIDENTIAL_TRANSFER_ACCOUNT_WITH_REGISTRY_OPERATION, &["TOKEN_2022_SOURCE", "TOKEN_2022_MINT", "ELGAMAL_REGISTRY_ADDRESS", "PUBKEY_VALIDITY_PROOF_CONTEXT"]),
];
}

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_execution.rs
// version: 7
// version: 9
//! Devnet classic SPL Token execution with stateful and canonical post-validation.
@@ -372,7 +372,7 @@ where
};
summary.materializations = rows
.into_iter()
.filter(|row| return row.source_decoder_name == "spl_token")
.filter(|row| return row.source_decoder_name == "spl.token")
.collect();
diagnostic.materialized =
!summary.plan.policy.post_execution_validation.materialization_required
@@ -973,18 +973,16 @@ pub(crate) fn decode_completed(summary: &kb_pipeline::DecodeReplaySummary) -> bo
#[cfg(test)]
mod tests {
fn local_devnet_profile() -> kb_config::ProfileConfig {
fn example_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");
return match crate::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(profile) => profile,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),
};
}
fn checked_transfer(fee_authority: &str) -> kb_lib::ExSplClassicTokenOperation {
@@ -1018,7 +1016,7 @@ mod tests {
#[test]
fn checked_transfer_plan_is_simulation_first_and_authorizes_exact_signers() {
let profile = local_devnet_profile();
let profile = example_devnet_profile();
let fee_payer = kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string());
let authority = kb_program_ids::VOTE_PROGRAM_ID;
let request =
@@ -1036,7 +1034,7 @@ mod tests {
#[test]
fn conversion_plan_does_not_invent_materialization_requirement() {
let profile = local_devnet_profile();
let profile = example_devnet_profile();
let request = crate::DevnetSplTokenExecutionRequest::new(
"token-3",
kb_lib::ExSplClassicTokenOperation::Instruction {
@@ -1092,7 +1090,7 @@ mod tests {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => "1".to_string(),
};
let mut profile = local_devnet_profile();
let mut profile = example_devnet_profile();
if let std::result::Result::Ok(directory) = std::env::var("KB_DEVNET_WALLET_DIR") {
profile.wallet.wallet_dir = directory;
}
@@ -1262,7 +1260,7 @@ mod tests {
},
std::result::Result::Err(_) => 9,
};
let mut profile = local_devnet_profile();
let mut profile = example_devnet_profile();
if let std::result::Result::Ok(directory) = std::env::var("KB_DEVNET_WALLET_DIR") {
profile.wallet.wallet_dir = directory;
}

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline-demo-scenarios/src/solana_token_lifecycle.rs
// version: 6
// version: 8
//! Controlled Devnet lifecycle for freshly prepared classic SPL Token accounts.
@@ -678,7 +678,7 @@ where
let recovered_count = rows
.iter()
.filter(|row| {
return row.source_decoder_name == "spl_token"
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());
})
@@ -756,7 +756,7 @@ fn materialized_operation_code(operation: &kb_lib::ExSplClassicTokenOperation) -
kb_lib::ExSplClassicTokenOperation::Instruction {
value: kb_lib::ExSplClassicTokenSingleOperation::InitializeAccount { .. },
} => "initialize_account3",
_ => match operation.operation_code().strip_prefix("spl_token.") {
_ => match operation.operation_code().strip_prefix("spl.token.") {
std::option::Option::Some(value) => value,
std::option::Option::None => operation.operation_code(),
},
@@ -1396,17 +1396,17 @@ mod tests {
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",
"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] {
@@ -1445,7 +1445,7 @@ mod tests {
#[test]
fn account_creation_plan_uses_exact_token_owner_space_spend_and_signers() {
let profile = local_devnet_profile();
let profile = example_devnet_profile();
let payer = key(kb_program_ids::SYSTEM_PROGRAM_ID);
let account = key(kb_program_ids::STAKE_PROGRAM_ID);
let plan = match super::build_account_creation_plan(
@@ -1459,7 +1459,7 @@ mod tests {
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.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);
@@ -1510,7 +1510,7 @@ mod tests {
{
panic!("controlled lifecycle requires KB_DEVNET_SPL_TOKEN_LIFECYCLE_SUBMIT=1");
}
let mut profile = local_devnet_profile();
let mut profile = example_devnet_profile();
if let std::result::Result::Ok(directory) = std::env::var("KB_DEVNET_WALLET_DIR") {
profile.wallet.wallet_dir = directory;
}
@@ -1686,18 +1686,16 @@ mod tests {
}
}
fn local_devnet_profile() -> kb_config::ProfileConfig {
fn example_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");
return match crate::resolve_demo_devnet_profile(&config, std::option::Option::None) {
std::result::Result::Ok(profile) => profile,
std::result::Result::Err(error) => panic!("Devnet profile resolution failed: {error}"),
};
}
fn required_pubkey_env(name: &str) -> kb_lib::MdPubkey {

View File

@@ -0,0 +1,462 @@
// file: kb-pipeline-demo-scenarios/src/token_2022_fixture.rs
// version: 2
//! Idempotent Token-2022 public scenario fixture preparation.
/// Default raw amount minted by the public MintToChecked scenario.
pub const DEFAULT_TOKEN_2022_MINT_AMOUNT_RAW: u64 = 10_000_000_000;
/// Default raw amount transferred by the public TransferChecked scenario.
pub const DEFAULT_TOKEN_2022_TRANSFER_AMOUNT_RAW: u64 = 1_000_000_000;
/// Default raw amount delegated by the public ApproveChecked scenario.
pub const DEFAULT_TOKEN_2022_APPROVE_AMOUNT_RAW: u64 = 2_000_000_000;
/// Default raw amount burned by the public BurnChecked scenario.
pub const DEFAULT_TOKEN_2022_BURN_AMOUNT_RAW: u64 = 1_000_000_000;
macro_rules! kb_try {
($expression:expr) => {
match $expression {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
}
};
}
/// Command-line options for one Token-2022 fixture preparation.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Token2022FixturePreparationOptions {
/// Devnet RPC URL used by Solana CLI commands.
pub rpc_url: std::string::String,
/// Persistent operator wallet keypair path.
pub wallet_path: std::path::PathBuf,
/// Wallet directory receiving public fixture files and temporary keypairs.
pub wallet_dir: std::path::PathBuf,
/// Mint decimals used by the public scenarios.
pub decimals: u8,
}
/// Public values written to one Token-2022 fixture file.
#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)]
pub struct Token2022FixturePreparationSummary {
/// Generated fixture path.
pub fixture_path: std::path::PathBuf,
/// Canonical Token-2022 Program ID.
pub program_id: std::string::String,
/// Token-2022 mint address.
pub mint: std::string::String,
/// Source Token-2022 account address.
pub source: std::string::String,
/// Destination Token-2022 account address.
pub destination: std::string::String,
/// Empty Token-2022 account reserved for CloseAccount.
pub close_account: std::string::String,
/// Delegate public key.
pub delegate: std::string::String,
/// Wallet authority public key.
pub authority: std::string::String,
/// Freeze authority public key.
pub freeze_authority: std::string::String,
/// Mint decimals.
pub decimals: u8,
/// Whether the fixture file was replaced during this invocation.
pub fixture_written: bool,
}
/// Creates or reuses one complete Token-2022 public scenario fixture.
pub async fn prepare_token_2022_fixture(
options: &crate::Token2022FixturePreparationOptions,
) -> kb_core::Result<crate::Token2022FixturePreparationSummary> {
kb_try!(validate_options(options));
let fixture_dir = options.wallet_dir.join("spl_token_2022_validation");
if let std::result::Result::Err(error) = tokio::fs::create_dir_all(&fixture_dir).await {
return std::result::Result::Err(kb_core::Error::io(format!(
"unable to create Token-2022 fixture directory {}: {error}",
fixture_dir.display()
)));
}
let wallet_path = kb_try!(path_text(&options.wallet_path));
let authority =
kb_try!(command_stdout("solana-keygen", &["pubkey", wallet_path.as_str()]).await);
let mint_keypair = fixture_dir.join("mint.json");
let source_keypair = fixture_dir.join("source-account.json");
let destination_keypair = fixture_dir.join("destination-account.json");
let close_account_keypair = fixture_dir.join("close-account.json");
let delegate_keypair = fixture_dir.join("delegate.json");
kb_try!(ensure_keypair(&mint_keypair).await);
kb_try!(ensure_keypair(&source_keypair).await);
kb_try!(ensure_keypair(&destination_keypair).await);
kb_try!(ensure_keypair(&close_account_keypair).await);
kb_try!(ensure_keypair(&delegate_keypair).await);
let mint = kb_try!(keypair_pubkey(&mint_keypair).await);
let source = kb_try!(keypair_pubkey(&source_keypair).await);
let destination = kb_try!(keypair_pubkey(&destination_keypair).await);
let close_account = kb_try!(keypair_pubkey(&close_account_keypair).await);
let delegate = kb_try!(keypair_pubkey(&delegate_keypair).await);
let mint_exists = kb_try!(account_exists(options.rpc_url.as_str(), mint.as_str()).await);
if !mint_exists {
let decimals = options.decimals.to_string();
let wallet_path = kb_try!(path_text(&options.wallet_path));
let mint_keypair_path = kb_try!(path_text(&mint_keypair));
kb_try!(
run_command(
"spl-token",
&[
"--url",
options.rpc_url.as_str(),
"--program-id",
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
"create-token",
"--enable-freeze",
"--decimals",
decimals.as_str(),
"--fee-payer",
wallet_path.as_str(),
"--mint-authority",
authority.as_str(),
mint_keypair_path.as_str()
]
)
.await
);
}
kb_try!(
ensure_token_account(
options,
mint.as_str(),
authority.as_str(),
&source_keypair,
source.as_str()
)
.await
);
kb_try!(
ensure_token_account(
options,
mint.as_str(),
authority.as_str(),
&destination_keypair,
destination.as_str()
)
.await
);
kb_try!(
ensure_token_account(
options,
mint.as_str(),
authority.as_str(),
&close_account_keypair,
close_account.as_str()
)
.await
);
let fixture_path = fixture_dir.join("fixture.env");
let summary = crate::Token2022FixturePreparationSummary {
fixture_path: fixture_path.clone(),
program_id: kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
mint,
source,
destination,
close_account,
delegate,
authority: authority.clone(),
freeze_authority: authority,
decimals: options.decimals,
fixture_written: true,
};
let contents = render_fixture(&summary);
let fixture_written =
kb_try!(write_fixture_atomically(&fixture_path, contents.as_bytes()).await);
return std::result::Result::Ok(crate::Token2022FixturePreparationSummary {
fixture_path,
program_id: summary.program_id,
mint: summary.mint,
source: summary.source,
destination: summary.destination,
close_account: summary.close_account,
delegate: summary.delegate,
authority: summary.authority,
freeze_authority: summary.freeze_authority,
decimals: summary.decimals,
fixture_written,
});
}
fn validate_options(options: &crate::Token2022FixturePreparationOptions) -> kb_core::Result<()> {
if options.rpc_url.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 fixture RPC URL must not be empty",
));
}
if !options.wallet_path.is_file() {
return std::result::Result::Err(kb_core::Error::config(format!(
"Token-2022 fixture wallet does not exist: {}",
options.wallet_path.display()
)));
}
if options.decimals > 18 {
return std::result::Result::Err(kb_core::Error::config(
"Token-2022 fixture decimals must be at most 18",
));
}
return std::result::Result::Ok(());
}
async fn ensure_keypair(path: &std::path::Path) -> kb_core::Result<()> {
if path.is_file() {
return std::result::Result::Ok(());
}
let keypair_path = kb_try!(path_text(path));
kb_try!(
run_command(
"solana-keygen",
&[
"new",
"--no-bip39-passphrase",
"--force",
"--silent",
"--outfile",
keypair_path.as_str()
]
)
.await
);
kb_try!(set_private_permissions(path).await);
return std::result::Result::Ok(());
}
async fn set_private_permissions(path: &std::path::Path) -> kb_core::Result<()> {
#[cfg(unix)]
{
let metadata = match tokio::fs::metadata(path).await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::io(format!(
"unable to read keypair metadata {}: {error}",
path.display()
)));
},
};
let mut permissions = metadata.permissions();
std::os::unix::fs::PermissionsExt::set_mode(&mut permissions, 0o600);
if let std::result::Result::Err(error) = tokio::fs::set_permissions(path, permissions).await
{
return std::result::Result::Err(kb_core::Error::io(format!(
"unable to protect keypair {}: {error}",
path.display()
)));
}
}
return std::result::Result::Ok(());
}
async fn keypair_pubkey(path: &std::path::Path) -> kb_core::Result<std::string::String> {
let keypair_path = kb_try!(path_text(path));
return command_stdout("solana-keygen", &["pubkey", keypair_path.as_str()]).await;
}
async fn account_exists(rpc_url: &str, address: &str) -> kb_core::Result<bool> {
let output = match tokio::process::Command::new("solana")
.args(["account", address, "--url", rpc_url, "--output", "json"])
.output()
.await
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::io(format!(
"unable to execute solana account: {error}"
)));
},
};
return std::result::Result::Ok(output.status.success());
}
async fn ensure_token_account(
options: &crate::Token2022FixturePreparationOptions,
mint: &str,
authority: &str,
keypair_path: &std::path::Path,
address: &str,
) -> kb_core::Result<()> {
let exists = kb_try!(account_exists(options.rpc_url.as_str(), address).await);
if exists {
return std::result::Result::Ok(());
}
let token_account_keypair = kb_try!(path_text(keypair_path));
let wallet_path = kb_try!(path_text(&options.wallet_path));
kb_try!(
run_command(
"spl-token",
&[
"--url",
options.rpc_url.as_str(),
"--program-id",
kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
"create-account",
mint,
token_account_keypair.as_str(),
"--owner",
authority,
"--fee-payer",
wallet_path.as_str()
]
)
.await
);
return std::result::Result::Ok(());
}
async fn command_stdout(program: &str, arguments: &[&str]) -> kb_core::Result<std::string::String> {
let output = match tokio::process::Command::new(program).args(arguments).output().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::io(format!(
"unable to execute {program}: {error}"
)));
},
};
if !output.status.success() {
return std::result::Result::Err(command_error(program, arguments, &output));
}
let value = match std::string::String::from_utf8(output.stdout) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"{program} returned non-UTF-8 output: {error}"
)));
},
};
return std::result::Result::Ok(value.trim().to_string());
}
async fn run_command(program: &str, arguments: &[&str]) -> kb_core::Result<()> {
let output = match tokio::process::Command::new(program).args(arguments).output().await {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::io(format!(
"unable to execute {program}: {error}"
)));
},
};
if !output.status.success() {
return std::result::Result::Err(command_error(program, arguments, &output));
}
return std::result::Result::Ok(());
}
fn command_error(
program: &str,
arguments: &[&str],
output: &std::process::Output,
) -> kb_core::Error {
let stderr = std::string::String::from_utf8_lossy(output.stderr.as_slice())
.trim()
.to_string();
let stdout = std::string::String::from_utf8_lossy(output.stdout.as_slice())
.trim()
.to_string();
let detail = if stderr.is_empty() { stdout } else { stderr };
return kb_core::Error::new(
"token_2022_fixture_command_failed",
format!(
"command {program} {} failed with status {}: {detail}",
arguments.join(" "),
output.status
),
);
}
fn path_text(path: &std::path::Path) -> kb_core::Result<std::string::String> {
return match path.to_str() {
std::option::Option::Some(value) => std::result::Result::Ok(value.to_string()),
std::option::Option::None => std::result::Result::Err(kb_core::Error::config(format!(
"path is not valid UTF-8: {}",
path.display()
))),
};
}
fn render_fixture(summary: &crate::Token2022FixturePreparationSummary) -> std::string::String {
return format!(
"export TOKEN_2022_PROGRAM={}\nexport TOKEN_2022_MINT={}\nexport TOKEN_2022_SOURCE={}\nexport TOKEN_2022_DESTINATION={}\nexport TOKEN_2022_CLOSE_ACCOUNT={}\nexport TOKEN_2022_DELEGATE={}\nexport TOKEN_2022_AUTHORITY={}\nexport TOKEN_2022_FREEZE_AUTHORITY={}\nexport TOKEN_2022_DECIMALS={}\nexport TOKEN_2022_MINT_AMOUNT_RAW={}\nexport TOKEN_2022_TRANSFER_AMOUNT_RAW={}\nexport TOKEN_2022_APPROVE_AMOUNT_RAW={}\nexport TOKEN_2022_BURN_AMOUNT_RAW={}\nexport KB_DEVNET_WALLET_ADDRESS={}\n",
summary.program_id,
summary.mint,
summary.source,
summary.destination,
summary.close_account,
summary.delegate,
summary.authority,
summary.freeze_authority,
summary.decimals,
crate::DEFAULT_TOKEN_2022_MINT_AMOUNT_RAW,
crate::DEFAULT_TOKEN_2022_TRANSFER_AMOUNT_RAW,
crate::DEFAULT_TOKEN_2022_APPROVE_AMOUNT_RAW,
crate::DEFAULT_TOKEN_2022_BURN_AMOUNT_RAW,
summary.authority
);
}
async fn write_fixture_atomically(
path: &std::path::Path,
contents: &[u8],
) -> kb_core::Result<bool> {
if let std::result::Result::Ok(existing) = tokio::fs::read(path).await {
if existing == contents {
return std::result::Result::Ok(false);
}
}
let temporary = path.with_extension("env.tmp");
if let std::result::Result::Err(error) = tokio::fs::write(&temporary, contents).await {
return std::result::Result::Err(kb_core::Error::io(format!(
"unable to write temporary Token-2022 fixture {}: {error}",
temporary.display()
)));
}
if let std::result::Result::Err(error) = tokio::fs::rename(&temporary, path).await {
return std::result::Result::Err(kb_core::Error::io(format!(
"unable to replace Token-2022 fixture {}: {error}",
path.display()
)));
}
return std::result::Result::Ok(true);
}
#[cfg(test)]
mod tests {
#[test]
fn rendered_fixture_contains_every_public_scenario_variable() {
let summary = super::Token2022FixturePreparationSummary {
fixture_path: std::path::PathBuf::from("fixture.env"),
program_id: kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.to_string(),
mint: "mint".to_string(),
source: "source".to_string(),
destination: "destination".to_string(),
close_account: "close".to_string(),
delegate: "delegate".to_string(),
authority: "authority".to_string(),
freeze_authority: "authority".to_string(),
decimals: 9,
fixture_written: true,
};
let fixture = super::render_fixture(&summary);
for scenario in crate::devnet_spl_validation_scenarios().into_iter().filter(|scenario| {
return scenario.family == crate::DevnetSplValidationFamily::Token2022Public;
}) {
for variable in scenario.required_fixture_variables {
assert!(fixture.lines().any(|line| return line.starts_with(format!("export {variable}=").as_str())), "missing {variable}");
}
}
assert!(fixture.contains("export TOKEN_2022_CLOSE_ACCOUNT=close\n"));
assert!(fixture.contains("export KB_DEVNET_WALLET_ADDRESS=authority\n"));
}
#[test]
fn options_reject_excessive_decimals() {
let options = super::Token2022FixturePreparationOptions {
rpc_url: "https://api.devnet.solana.com".to_string(),
wallet_path: std::path::PathBuf::from("missing.json"),
wallet_dir: std::path::PathBuf::from("wallets"),
decimals: 19,
};
let error = super::validate_options(&options);
assert!(error.is_err());
}
}