v0.4.7-pre.011
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/lib.rs
|
||||
// version: 8
|
||||
// version: 9
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -12,6 +12,7 @@ mod environment;
|
||||
mod solana_ata_execution;
|
||||
mod solana_execution;
|
||||
mod solana_memo_execution;
|
||||
mod solana_metaplex_token_metadata_devnet_execution;
|
||||
mod solana_metaplex_token_metadata_scenarios;
|
||||
mod solana_metaplex_token_metadata_validation;
|
||||
mod solana_token_2022_devnet_execution;
|
||||
@@ -67,6 +68,14 @@ pub use self::solana_memo_execution::DevnetMemoExecutionRequest;
|
||||
pub use self::solana_memo_execution::DevnetMemoExecutionSummary;
|
||||
/// Executes one SPL Memo v4 Devnet simulation or explicitly authorized submission.
|
||||
pub use self::solana_memo_execution::execute_devnet_memo;
|
||||
/// Complete request for one real Devnet Metaplex Token Metadata execution.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionRequest;
|
||||
/// Complete evidence produced by one real Devnet Metaplex Token Metadata execution.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::DevnetMetaplexTokenMetadataExecutionSummary;
|
||||
/// Executes one real Devnet Metaplex simulation or explicitly authorized submission.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::execute_devnet_metaplex_token_metadata;
|
||||
/// Simulates one current Metaplex operation against a real Devnet endpoint.
|
||||
pub use self::solana_metaplex_token_metadata_devnet_execution::simulate_devnet_metaplex_token_metadata;
|
||||
/// Stable asset family covered by one Metaplex scenario.
|
||||
pub use self::solana_metaplex_token_metadata_scenarios::MetaplexTokenMetadataAssetFamily;
|
||||
/// One stable Metaplex scenario reusable by automated tests and demo adapters.
|
||||
@@ -83,6 +92,10 @@ pub use self::solana_metaplex_token_metadata_validation::MAX_METAPLEX_TOKEN_META
|
||||
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataCrossValidationCase;
|
||||
/// Closed Metaplex cross-validation corpus for `0.4.7-pre.010`.
|
||||
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataCrossValidationMatrix;
|
||||
/// Closed inventory of every current Metaplex operation requiring Devnet coverage.
|
||||
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataDevnetExecutionMatrix;
|
||||
/// One current Metaplex operation in the closed Devnet execution matrix.
|
||||
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataDevnetExecutionOperation;
|
||||
/// Stable failure category exercised by one negative case.
|
||||
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataFailureClass;
|
||||
/// Expected transaction outcome for one cross-validation case.
|
||||
@@ -99,10 +112,14 @@ pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataVa
|
||||
pub use self::solana_metaplex_token_metadata_validation::MetaplexTokenMetadataValidationStatus;
|
||||
/// Loads and validates the closed Metaplex cross-validation corpus.
|
||||
pub use self::solana_metaplex_token_metadata_validation::load_metaplex_token_metadata_cross_validation_matrix;
|
||||
/// Loads and validates the closed Devnet execution matrix.
|
||||
pub use self::solana_metaplex_token_metadata_validation::load_metaplex_token_metadata_devnet_execution_matrix;
|
||||
/// Loads and validates the canonical Metaplex validation matrix.
|
||||
pub use self::solana_metaplex_token_metadata_validation::load_metaplex_token_metadata_validation_matrix;
|
||||
/// Validates the closed Metaplex cross-validation corpus and evidence claims.
|
||||
pub use self::solana_metaplex_token_metadata_validation::validate_metaplex_token_metadata_cross_validation_matrix;
|
||||
/// Validates exact current-operation coverage and conservative network statuses.
|
||||
pub use self::solana_metaplex_token_metadata_validation::validate_metaplex_token_metadata_devnet_execution_matrix;
|
||||
/// Validates the canonical Metaplex validation matrix.
|
||||
pub use self::solana_metaplex_token_metadata_validation::validate_metaplex_token_metadata_validation_matrix;
|
||||
/// Complete request for one Devnet Token-2022 execution.
|
||||
|
||||
@@ -0,0 +1,608 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_devnet_execution.rs
|
||||
// version: 1
|
||||
|
||||
//! Real Devnet simulation and submission for current Metaplex Token Metadata operations.
|
||||
|
||||
/// Complete request for one real Devnet Metaplex Token Metadata execution.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetMetaplexTokenMetadataExecutionRequest {
|
||||
/// Stable caller-provided execution identifier.
|
||||
pub intent_id: std::string::String,
|
||||
/// Endpoint role used for reads, balance, blockhash and fee queries.
|
||||
pub query_role: std::string::String,
|
||||
/// Endpoint role used for simulation, submission and confirmation.
|
||||
pub transaction_role: std::string::String,
|
||||
/// Exact current Metaplex operation.
|
||||
pub operation: kb_lib::ExMetaplexTokenMetadataOperation,
|
||||
/// Bounded state reads required before execution.
|
||||
pub preflight_reads: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
|
||||
/// Bounded state reads repeated after confirmed submission.
|
||||
pub postcondition_reads: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadRequest>,
|
||||
/// Explicitly authorizes signing and submission after successful simulation.
|
||||
pub submit: bool,
|
||||
/// Explicit operator confirmation required for submission.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
impl crate::DevnetMetaplexTokenMetadataExecutionRequest {
|
||||
/// Creates a conservative simulation-only request.
|
||||
pub fn new(
|
||||
intent_id: impl std::convert::Into<std::string::String>,
|
||||
operation: kb_lib::ExMetaplexTokenMetadataOperation,
|
||||
) -> Self {
|
||||
return Self {
|
||||
intent_id: intent_id.into(),
|
||||
query_role: "http_queries".to_string(),
|
||||
transaction_role: "http_transactions".to_string(),
|
||||
operation,
|
||||
preflight_reads: std::vec::Vec::new(),
|
||||
postcondition_reads: std::vec::Vec::new(),
|
||||
submit: false,
|
||||
operator_confirmed: false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Validates request-local bounds and rejects deprecated operations.
|
||||
pub fn validate(&self) -> kb_core::Result<()> {
|
||||
if self.intent_id.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Devnet Metaplex execution intent id must not be empty",
|
||||
));
|
||||
}
|
||||
if self.query_role.trim().is_empty() || self.transaction_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Devnet Metaplex execution endpoint roles must not be empty",
|
||||
));
|
||||
}
|
||||
if self.preflight_reads.len() > kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS
|
||||
|| self.postcondition_reads.len()
|
||||
> kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_PREFLIGHT_ACCOUNTS
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Devnet Metaplex execution stateful read limit exceeded",
|
||||
));
|
||||
}
|
||||
let operation_code = self.operation.operation_code();
|
||||
if kb_lib::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES.contains(&operation_code) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_metaplex_devnet_deprecated_operation_forbidden",
|
||||
"automatic Devnet campaigns do not execute deprecated Metaplex operations",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete evidence produced by one real Devnet Metaplex execution.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetMetaplexTokenMetadataExecutionSummary {
|
||||
/// Profile used by the orchestration.
|
||||
pub profile_name: std::string::String,
|
||||
/// Exact classified cluster.
|
||||
pub cluster: kb_lib::ExApiExecutionCluster,
|
||||
/// Genesis hash returned by the selected endpoint.
|
||||
pub genesis_hash: std::string::String,
|
||||
/// Non-secret persistent wallet description.
|
||||
pub wallet: kb_wallet::WalletSummary,
|
||||
/// Wallet balance observed before planning.
|
||||
pub balance_lamports: u64,
|
||||
/// Stateful snapshots observed before simulation.
|
||||
pub before: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadResult>,
|
||||
/// Stateful preflight report bound to the exact plan.
|
||||
pub stateful_preflight: kb_pipeline::MetaplexTokenMetadataPreflightReport,
|
||||
/// Exact prepared Metaplex plan.
|
||||
pub plan: kb_lib::ExApiPreparedExecutionPlan,
|
||||
/// Recent blockhash used by the exact transaction.
|
||||
pub latest_blockhash: kb_onchain_transport::LatestBlockhashResult,
|
||||
/// Fee estimate for the exact compiled message.
|
||||
pub fee: kb_onchain_transport::FeeForMessageResult,
|
||||
/// Exact real RPC simulation result.
|
||||
pub simulation: kb_lib::ExApiExecutionSimulationResult,
|
||||
/// Readiness report proving exact-message simulation and signer resolution.
|
||||
pub readiness: kb_pipeline::MetaplexTokenMetadataExecutionReadinessReport,
|
||||
/// Submission result when explicitly authorized.
|
||||
pub send_result: std::option::Option<kb_lib::ExApiExecutionSendResult>,
|
||||
/// Confirmation result when submitted.
|
||||
pub confirmation: std::option::Option<kb_lib::ExApiExecutionConfirmationResult>,
|
||||
/// Stateful snapshots observed after confirmed submission.
|
||||
pub after: std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadResult>,
|
||||
}
|
||||
|
||||
struct PreparedMetaplexExecution {
|
||||
wallet: kb_wallet::TemporaryWallet,
|
||||
unsigned: kb_lib::ExSolanaUnsignedTransaction,
|
||||
evidence: kb_lib::ExSolanaSimulationEvidence,
|
||||
summary: crate::DevnetMetaplexTokenMetadataExecutionSummary,
|
||||
}
|
||||
|
||||
/// Simulates one current Metaplex operation against a real Devnet endpoint.
|
||||
pub async fn simulate_devnet_metaplex_token_metadata<O>(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DevnetMetaplexTokenMetadataExecutionSummary>
|
||||
where
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
if request.submit {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"simulate_devnet_metaplex_token_metadata requires submit=false",
|
||||
));
|
||||
}
|
||||
let prepared =
|
||||
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(prepared.summary);
|
||||
}
|
||||
|
||||
/// Executes one real Devnet Metaplex simulation or explicitly authorized submission.
|
||||
pub async fn execute_devnet_metaplex_token_metadata<O>(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DevnetMetaplexTokenMetadataExecutionSummary>
|
||||
where
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
let prepared =
|
||||
match prepare_execution(http_pool, profile, workspace_root, request, observer).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if !request.submit {
|
||||
return std::result::Result::Ok(prepared.summary);
|
||||
}
|
||||
if !prepared.summary.simulation.success {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_simulation_failed",
|
||||
crate::simulation_failure_message(&prepared.summary.simulation),
|
||||
));
|
||||
}
|
||||
if let std::result::Result::Err(error) = validate_profile_wallet_signers(
|
||||
prepared.unsigned.required_signer_pubkeys(),
|
||||
prepared.summary.wallet.public_key.as_str(),
|
||||
) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let send_evaluation = match kb_lib::ExSafetyChecker
|
||||
.evaluate_send(&prepared.summary.plan, &prepared.summary.simulation)
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if send_evaluation.decision == kb_lib::ExSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_send_denied",
|
||||
crate::violation_message(send_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let signed = match prepared
|
||||
.unsigned
|
||||
.sign_after_simulation(&prepared.evidence, &[prepared.wallet.as_signer()])
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = signed.verify_signatures() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let signature = signed.primary_signature().clone();
|
||||
let mut summary = prepared.summary;
|
||||
let send_config = match kb_onchain_transport::SendTransactionConfig::from_execution_config(
|
||||
&profile.execution,
|
||||
std::option::Option::Some(summary.latest_blockhash.context.slot),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let sent = match http_pool
|
||||
.send_transaction_for_role(
|
||||
request.transaction_role.as_str(),
|
||||
signed.transaction_base64().as_str(),
|
||||
&signature,
|
||||
&send_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
summary.send_result =
|
||||
std::option::Option::Some(sent.to_execution_result(kb_lib::ExApiExecutionCluster::Devnet));
|
||||
let confirmation_config =
|
||||
match kb_onchain_transport::ConfirmTransactionConfig::from_execution_config(
|
||||
&profile.execution,
|
||||
std::option::Option::Some(summary.latest_blockhash.last_valid_block_height),
|
||||
std::option::Option::Some(summary.latest_blockhash.context.slot),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let confirmation = match http_pool
|
||||
.confirm_transaction_for_roles(
|
||||
request.transaction_role.as_str(),
|
||||
request.query_role.as_str(),
|
||||
kb_lib::ExApiExecutionCluster::Devnet,
|
||||
&signature,
|
||||
&confirmation_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let confirmed = matches!(
|
||||
confirmation.status,
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||
);
|
||||
summary.confirmation = std::option::Option::Some(confirmation);
|
||||
if confirmed {
|
||||
summary.after = match read_snapshots(http_pool, &request.postcondition_reads).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
}
|
||||
return std::result::Result::Ok(summary);
|
||||
}
|
||||
|
||||
async fn prepare_execution<O>(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<PreparedMetaplexExecution>
|
||||
where
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
if let std::result::Result::Err(error) = request.validate() {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) = validate_profile(profile, request) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if let std::result::Result::Err(error) =
|
||||
crate::ensure_not_cancelled(observer, "metaplex_validate")
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let genesis = match http_pool.get_genesis_hash_for_role(request.query_role.as_str()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if genesis.classified_cluster
|
||||
!= std::option::Option::Some(kb_lib::ExApiExecutionCluster::Devnet)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_cluster_mismatch",
|
||||
format!(
|
||||
"expected Devnet genesis hash but endpoint returned {} classified as {:?}",
|
||||
genesis.genesis_hash, genesis.classified_cluster
|
||||
),
|
||||
));
|
||||
}
|
||||
let wallet = match crate::load_profile_wallet(profile, workspace_root).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let wallet_summary = wallet.summary();
|
||||
let fee_payer = kb_lib::MdPubkey(wallet_summary.public_key.clone());
|
||||
let balance = match http_pool
|
||||
.get_balance_for_role(
|
||||
request.query_role.as_str(),
|
||||
&fee_payer,
|
||||
&kb_onchain_transport::GetBalanceConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if balance.lamports < profile.execution.max_fee_lamports {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_balance_insufficient",
|
||||
"Devnet wallet balance is below the configured fee ceiling",
|
||||
));
|
||||
}
|
||||
let plan = match build_plan(profile, request, fee_payer.clone()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let before = match read_snapshots(http_pool, &request.preflight_reads).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let stateful_preflight = match kb_pipeline::inspect_metaplex_token_metadata_preflight(
|
||||
&kb_pipeline::MetaplexTokenMetadataPreflightRequest {
|
||||
plan: plan.clone(),
|
||||
snapshots: before.clone(),
|
||||
allow_deprecated_operation: false,
|
||||
},
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let plan_evaluation = match kb_lib::ExSafetyChecker.evaluate_prepared_plan(&plan) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if plan_evaluation.decision == kb_lib::ExSafetyDecision::Deny {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_plan_denied",
|
||||
crate::violation_message(plan_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let latest_blockhash = match http_pool
|
||||
.get_latest_blockhash_for_role(
|
||||
request.query_role.as_str(),
|
||||
&kb_onchain_transport::GetLatestBlockhashConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let unsigned = match kb_lib::executor_solana_build_legacy_transaction(
|
||||
&plan,
|
||||
latest_blockhash.blockhash.as_str(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = validate_profile_wallet_signers(
|
||||
unsigned.required_signer_pubkeys(),
|
||||
wallet_summary.public_key.as_str(),
|
||||
) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let fee = match http_pool
|
||||
.get_fee_for_message_for_role(
|
||||
request.query_role.as_str(),
|
||||
unsigned.message_base64().as_str(),
|
||||
&kb_onchain_transport::GetFeeForMessageConfig::new(
|
||||
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let unsigned_base64 = match unsigned.transaction_base64() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let simulation_config = match kb_onchain_transport::SimulateTransactionConfig::new(
|
||||
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
|
||||
false,
|
||||
false,
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
true,
|
||||
std::option::Option::None,
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
crate::emit(
|
||||
observer,
|
||||
crate::SolanaExecutionProgressLevel::Info,
|
||||
"metaplex_token_metadata_simulation",
|
||||
format!("simulating exact Metaplex message {}", unsigned.message_hash()),
|
||||
std::option::Option::None,
|
||||
);
|
||||
let simulation_rpc = match http_pool
|
||||
.simulate_transaction_for_role(
|
||||
request.transaction_role.as_str(),
|
||||
unsigned_base64.as_str(),
|
||||
&simulation_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let simulation = simulation_rpc.to_execution_result(
|
||||
kb_lib::ExApiExecutionCluster::Devnet,
|
||||
kb_lib::ExApiExecutionBlockhashKind::Latest,
|
||||
std::option::Option::Some(
|
||||
simulation_rpc.context.slot.saturating_sub(latest_blockhash.context.slot),
|
||||
),
|
||||
std::option::Option::None,
|
||||
std::option::Option::None,
|
||||
std::option::Option::Some(&fee),
|
||||
);
|
||||
let readiness = match kb_pipeline::validate_metaplex_token_metadata_execution_readiness(
|
||||
&kb_pipeline::MetaplexTokenMetadataExecutionReadinessRequest {
|
||||
plan: plan.clone(),
|
||||
preflight: stateful_preflight.clone(),
|
||||
message_hash: unsigned.message_hash().to_string(),
|
||||
simulated_message_hash: unsigned.message_hash().to_string(),
|
||||
simulated: true,
|
||||
simulation_succeeded: simulation.success,
|
||||
resolved_signers: vec![fee_payer],
|
||||
submit: request.submit,
|
||||
operator_confirmed: request.operator_confirmed,
|
||||
},
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let evidence = unsigned.bind_simulation(simulation.clone());
|
||||
return std::result::Result::Ok(PreparedMetaplexExecution {
|
||||
wallet,
|
||||
unsigned,
|
||||
evidence,
|
||||
summary: crate::DevnetMetaplexTokenMetadataExecutionSummary {
|
||||
profile_name: profile.name.clone(),
|
||||
cluster: kb_lib::ExApiExecutionCluster::Devnet,
|
||||
genesis_hash: genesis.genesis_hash,
|
||||
wallet: wallet_summary,
|
||||
balance_lamports: balance.lamports,
|
||||
before,
|
||||
stateful_preflight,
|
||||
plan,
|
||||
latest_blockhash,
|
||||
fee,
|
||||
simulation,
|
||||
readiness,
|
||||
send_result: std::option::Option::None,
|
||||
confirmation: std::option::Option::None,
|
||||
after: std::vec::Vec::new(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async fn read_snapshots(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
requests: &[kb_pipeline::MetaplexTokenMetadataStatefulReadRequest],
|
||||
) -> kb_core::Result<std::vec::Vec<kb_pipeline::MetaplexTokenMetadataStatefulReadResult>> {
|
||||
let mut results = std::vec::Vec::with_capacity(requests.len());
|
||||
for request in requests {
|
||||
let value =
|
||||
match kb_pipeline::read_metaplex_token_metadata_stateful_snapshot(http_pool, request)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
results.push(value);
|
||||
}
|
||||
return std::result::Result::Ok(results);
|
||||
}
|
||||
|
||||
fn build_plan(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
|
||||
fee_payer: kb_lib::MdPubkey,
|
||||
) -> kb_core::Result<kb_lib::ExApiPreparedExecutionPlan> {
|
||||
let intent = kb_lib::ExMetaplexTokenMetadataExecutionIntent {
|
||||
intent_id: request.intent_id.clone(),
|
||||
fee_payer: fee_payer.clone(),
|
||||
max_rent_lamports: profile.execution.devnet_max_spend_lamports,
|
||||
policy: kb_lib::ExApiExecutionPolicy {
|
||||
cluster: kb_lib::ExApiExecutionClusterPolicy {
|
||||
expected_cluster: kb_lib::ExApiExecutionCluster::Devnet,
|
||||
allow_mainnet: false,
|
||||
mainnet_confirmation: false,
|
||||
},
|
||||
simulation: kb_lib::ExApiExecutionSimulationPolicy::Required,
|
||||
blockhash: kb_lib::ExApiExecutionBlockhashPolicy {
|
||||
kind: kb_lib::ExApiExecutionBlockhashKind::Latest,
|
||||
max_age_slots: std::option::Option::Some(
|
||||
profile.execution.recent_blockhash_max_age_slots,
|
||||
),
|
||||
nonce_account: std::option::Option::None,
|
||||
nonce_authority: std::option::Option::None,
|
||||
},
|
||||
cost_limit: kb_lib::ExApiExecutionCostLimit {
|
||||
max_spend_lamports: std::option::Option::Some(
|
||||
profile.execution.devnet_max_spend_lamports,
|
||||
),
|
||||
max_fee_lamports: std::option::Option::Some(profile.execution.max_fee_lamports),
|
||||
max_compute_unit_price_micro_lamports: std::option::Option::Some(
|
||||
profile.execution.max_compute_unit_price_micro_lamports,
|
||||
),
|
||||
},
|
||||
authorized_signers: vec![fee_payer],
|
||||
dry_run: !request.submit,
|
||||
post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy {
|
||||
canonical_insert_required: request.submit,
|
||||
core_extraction_required: request.submit,
|
||||
decode_replay_required: request.submit,
|
||||
materialization_required: request.submit,
|
||||
},
|
||||
},
|
||||
allow_deprecated_operation: false,
|
||||
operation: request.operation.clone(),
|
||||
};
|
||||
return kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&kb_lib::ExMetadataMetaplexTokenMetadataExecutor,
|
||||
&intent,
|
||||
);
|
||||
}
|
||||
|
||||
fn validate_profile(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
request: &crate::DevnetMetaplexTokenMetadataExecutionRequest,
|
||||
) -> kb_core::Result<()> {
|
||||
if profile.wallet.cluster != "devnet" {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Metaplex Devnet orchestration requires a Devnet wallet profile",
|
||||
));
|
||||
}
|
||||
if !profile.wallet.temporary_wallet_enabled || !profile.wallet.temporary_wallet_persist {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Metaplex Devnet orchestration requires an enabled persistent temporary wallet",
|
||||
));
|
||||
}
|
||||
if !profile.execution.require_simulation {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Metaplex Devnet orchestration requires simulation",
|
||||
));
|
||||
}
|
||||
if request.submit && !profile.wallet.devnet_send_enabled {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Devnet transaction submission is disabled by the wallet profile",
|
||||
));
|
||||
}
|
||||
if request.submit
|
||||
&& profile.execution.require_operator_confirmation
|
||||
&& !request.operator_confirmed
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Metaplex Devnet submission requires explicit operator confirmation",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn validate_profile_wallet_signers(
|
||||
required_signers: &[std::string::String],
|
||||
wallet_pubkey: &str,
|
||||
) -> kb_core::Result<()> {
|
||||
if required_signers.iter().any(|value| return value.as_str() != wallet_pubkey) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_metaplex_external_signer_unavailable",
|
||||
format!(
|
||||
"the Devnet Metaplex orchestrator can sign only with profile wallet {}; required signers are {}",
|
||||
wallet_pubkey,
|
||||
required_signers.join(",")
|
||||
),
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn request_rejects_deprecated_operations_and_empty_ids() {
|
||||
let deprecated = crate::DevnetMetaplexTokenMetadataExecutionRequest::new(
|
||||
"deprecated",
|
||||
kb_lib::ExMetaplexTokenMetadataOperation::PuffMetadata {
|
||||
metadata: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
|
||||
},
|
||||
);
|
||||
assert!(deprecated.validate().is_err());
|
||||
let mut empty = deprecated;
|
||||
empty.intent_id.clear();
|
||||
assert!(empty.validate().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signer_guard_rejects_external_authorities() {
|
||||
assert!(super::validate_profile_wallet_signers(&["wallet".to_string()], "wallet").is_ok());
|
||||
assert!(
|
||||
super::validate_profile_wallet_signers(&["external".to_string()], "wallet").is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/solana_metaplex_token_metadata_validation.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Machine-readable Metaplex Token Metadata validation contract.
|
||||
|
||||
@@ -438,3 +438,162 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One current Metaplex operation in the closed Devnet execution matrix.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MetaplexTokenMetadataDevnetExecutionOperation {
|
||||
/// Stable instruction discriminator.
|
||||
pub discriminator: u8,
|
||||
/// Official instruction name.
|
||||
pub name: std::string::String,
|
||||
/// Stable executor operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Whether the operation is deprecated.
|
||||
pub deprecated: bool,
|
||||
/// Prerelease responsible for the campaign.
|
||||
pub campaign_prerelease: std::string::String,
|
||||
/// Implementation status of the reusable runner.
|
||||
pub runner_status: std::string::String,
|
||||
/// Exact observed network status.
|
||||
pub network_status: std::string::String,
|
||||
/// Evidence required after simulation.
|
||||
pub required_evidence: std::vec::Vec<std::string::String>,
|
||||
/// Additional evidence required after submission.
|
||||
pub submission_evidence: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// Closed inventory of every current Metaplex operation requiring Devnet coverage.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MetaplexTokenMetadataDevnetExecutionMatrix {
|
||||
/// Matrix schema version.
|
||||
pub matrix_version: u32,
|
||||
/// Owning prerelease.
|
||||
pub milestone: std::string::String,
|
||||
/// Canonical Token Metadata program ID.
|
||||
pub program_id: std::string::String,
|
||||
/// Ordered current operations.
|
||||
pub operations: std::vec::Vec<crate::MetaplexTokenMetadataDevnetExecutionOperation>,
|
||||
}
|
||||
|
||||
/// Loads and validates the closed Devnet execution matrix.
|
||||
pub fn load_metaplex_token_metadata_devnet_execution_matrix()
|
||||
-> kb_core::Result<crate::MetaplexTokenMetadataDevnetExecutionMatrix> {
|
||||
let matrix = match serde_json::from_str::<crate::MetaplexTokenMetadataDevnetExecutionMatrix>(
|
||||
include_str!(
|
||||
"../../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_DEVNET_EXECUTION_MATRIX.json"
|
||||
),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_devnet_execution_matrix_invalid_json",
|
||||
format!("Metaplex Devnet execution matrix JSON is invalid: {error}"),
|
||||
));
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) =
|
||||
validate_metaplex_token_metadata_devnet_execution_matrix(&matrix)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(matrix);
|
||||
}
|
||||
|
||||
/// Validates exact current-operation coverage and conservative network statuses.
|
||||
pub fn validate_metaplex_token_metadata_devnet_execution_matrix(
|
||||
matrix: &crate::MetaplexTokenMetadataDevnetExecutionMatrix,
|
||||
) -> kb_core::Result<()> {
|
||||
if matrix.matrix_version != 1
|
||||
|| matrix.milestone != "0.4.7-pre.011"
|
||||
|| matrix.program_id != kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
|
||||
|| matrix.operations.len() != 20
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_devnet_execution_matrix_contract_mismatch",
|
||||
"Metaplex Devnet matrix must declare exactly 20 current operations for pre.011",
|
||||
));
|
||||
}
|
||||
let expected = [
|
||||
"metadata.metaplex_token_metadata.create_escrow_account",
|
||||
"metadata.metaplex_token_metadata.close_escrow_account",
|
||||
"metadata.metaplex_token_metadata.transfer_out_of_escrow",
|
||||
"metadata.metaplex_token_metadata.burn",
|
||||
"metadata.metaplex_token_metadata.create",
|
||||
"metadata.metaplex_token_metadata.mint",
|
||||
"metadata.metaplex_token_metadata.delegate",
|
||||
"metadata.metaplex_token_metadata.revoke",
|
||||
"metadata.metaplex_token_metadata.lock",
|
||||
"metadata.metaplex_token_metadata.unlock",
|
||||
"metadata.metaplex_token_metadata.migrate",
|
||||
"metadata.metaplex_token_metadata.transfer",
|
||||
"metadata.metaplex_token_metadata.update",
|
||||
"metadata.metaplex_token_metadata.use",
|
||||
"metadata.metaplex_token_metadata.verify",
|
||||
"metadata.metaplex_token_metadata.unverify",
|
||||
"metadata.metaplex_token_metadata.collect",
|
||||
"metadata.metaplex_token_metadata.print",
|
||||
"metadata.metaplex_token_metadata.resize",
|
||||
"metadata.metaplex_token_metadata.close_accounts",
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
let observed = matrix
|
||||
.operations
|
||||
.iter()
|
||||
.map(|operation| return operation.operation_code.as_str())
|
||||
.collect::<std::collections::BTreeSet<&str>>();
|
||||
if observed != expected {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_devnet_execution_matrix_operation_mismatch",
|
||||
"Metaplex Devnet matrix must cover the exact 20 current operation codes",
|
||||
));
|
||||
}
|
||||
for operation in &matrix.operations {
|
||||
if operation.deprecated
|
||||
|| operation.required_evidence.is_empty()
|
||||
|| operation.submission_evidence.is_empty()
|
||||
|| !matches!(
|
||||
operation.network_status.as_str(),
|
||||
"not_run" | "simulated" | "confirmed" | "unavailable"
|
||||
)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"metaplex_devnet_execution_matrix_entry_invalid",
|
||||
"Metaplex Devnet entries must be current, evidenced and conservatively classified",
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod devnet_execution_matrix_tests {
|
||||
#[test]
|
||||
fn devnet_execution_matrix_is_closed_current_and_conservative() {
|
||||
let result = crate::load_metaplex_token_metadata_devnet_execution_matrix();
|
||||
assert!(result.is_ok());
|
||||
let matrix = if let std::result::Result::Ok(value) = result {
|
||||
value
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
assert_eq!(matrix.operations.len(), 20);
|
||||
assert_eq!(
|
||||
matrix
|
||||
.operations
|
||||
.iter()
|
||||
.filter(|operation| return operation.campaign_prerelease == "0.4.7-pre.011")
|
||||
.count(),
|
||||
4,
|
||||
);
|
||||
assert!(matrix.operations.iter().all(|operation| return !operation.deprecated));
|
||||
assert!(
|
||||
matrix
|
||||
.operations
|
||||
.iter()
|
||||
.all(|operation| return operation.network_status == "not_run")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user