v0.1.0-pre.062
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
462
kb-pipeline-demo-scenarios/src/token_2022_fixture.rs
Normal file
462
kb-pipeline-demo-scenarios/src/token_2022_fixture.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user