v0.4.8-pre.009
This commit is contained in:
@@ -1,8 +1,21 @@
|
||||
<!-- file: kb-pipeline-demo-scenarios/CHANGELOG.md -->
|
||||
<!-- version: 27 -->
|
||||
<!-- version: 30 -->
|
||||
|
||||
# CHANGELOG — kb-pipeline-demo-scenarios
|
||||
|
||||
## 0.4.8-pre.009 — scénarios Devnet Solana Program Metadata
|
||||
|
||||
- corrige les derniers diagnostics Clippy des tests : `str::len()` direct pour les seeds UTF-8 et `return` explicite dans la closure de statut de validation ;
|
||||
- corrige les tests de réconciliation des opérations : comparaison de références `&str` et durée de vie de l’inventaire de scénarios ;
|
||||
- documente la crate du test d’API publique externe afin de conserver `missing_docs` propre ;
|
||||
- ajoute deux parcours ordonnés couvrant exactement les neuf opérations stables de `ProgM6…` ;
|
||||
- prépare deux PDA non canoniques préfinancés, un `Buffer` et un compte `Metadata`, à partir du wallet persistant du profil Devnet ;
|
||||
- ajoute un runner simulation-first avec préflight stateful, `ExSafetyChecker`, signature, soumission, confirmation, lectures avant/après et matérialisation des snapshots ;
|
||||
- ajoute une campagne confirmée réutilisable qui exécute les neuf étapes sans déplacer l’orchestration dans le desktop ;
|
||||
- distingue la preuve `materialized_snapshot` de la preuve terminale `account_absence` après `Close` ;
|
||||
- ajoute une matrice conservative dont tous les statuts réseau restent `not_run` avant exécution réelle ;
|
||||
- conserve `kb-pipeline` indépendant de cette crate et ne modifie pas encore `demo_execution_metadata`.
|
||||
|
||||
## 0.4.7-pre.016
|
||||
|
||||
- généralise README et USAGE autour des groupes fonctionnels ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-pipeline-demo-scenarios/README.md -->
|
||||
<!-- version: 7 -->
|
||||
<!-- version: 8 -->
|
||||
|
||||
# kb-pipeline-demo-scenarios
|
||||
|
||||
@@ -19,6 +19,8 @@ La crate contient :
|
||||
- fournir des tests automatisés parallèles aux démonstrations UI, sans déplacer les scénarios UI hors du desktop ;
|
||||
- conserver la séparation entre preuve synthétique, simulation RPC, soumission confirmée et postcondition stateful.
|
||||
|
||||
La crate couvre déjà les scénarios Solana Core et SPL historiques, le corpus synthétique Metaplex pour NFT, SFT, token fongible, collection et pNFT, ainsi que deux parcours Solana Program Metadata couvrant les neuf opérations stables. La fixture `ProgM6…` prépare des PDA non canoniques préfinancés et la campagne réutilisable conserve simulation, soumission, confirmation, lectures stateful et projections matérialisées.
|
||||
|
||||
La crate couvre déjà les scénarios Solana Core et SPL historiques ainsi que le corpus synthétique Metaplex pour NFT, SFT, token fongible, collection et pNFT. Un exécuteur Devnet réel et réutilisable est disponible pour les opérations Metaplex courantes pilotées par un seul wallet de profil. Il réalise le contrôle du genesis hash, les lectures stateful bornées, le préflight, la simulation RPC exacte et, après confirmation explicite, la soumission, la confirmation et les lectures avant/après. Le runner Devnet sait construire les opérations courantes, tandis que les parcours réellement préparés fournissent leurs fixtures, preuves RPC et postconditions. Les validations spécialisées restent qualifiées séparément au lieu d’être déduites de la seule présence d’un builder.
|
||||
|
||||
## Binaire CLI
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
<!-- file: kb-pipeline-demo-scenarios/TODO.md -->
|
||||
<!-- version: 10 -->
|
||||
<!-- version: 11 -->
|
||||
|
||||
# TODO — kb-pipeline-demo-scenarios
|
||||
|
||||
## Validation Devnet Solana Program Metadata
|
||||
|
||||
- [ ] Exécuter la campagne confirmée des neuf opérations avec un wallet Devnet dédié.
|
||||
- [ ] Conserver les signatures, simulations, lectures avant/après et postconditions dans la matrice de validation.
|
||||
- [ ] Confirmer spécifiquement `Trim` et `Extend`, absentes de l’échantillon mainnet `pre.008`.
|
||||
- [ ] Vérifier le nettoyage ou la fermeture complète des deux PDA de fixture.
|
||||
|
||||
## Validation Devnet complémentaire Metaplex Token Metadata
|
||||
|
||||
- [ ] Préparer les fixtures d’édition imprimée et de burn NFT.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-pipeline-demo-scenarios/USAGE.md -->
|
||||
<!-- version: 9 -->
|
||||
<!-- version: 10 -->
|
||||
|
||||
# Utilisation de kb-pipeline-demo-scenarios
|
||||
|
||||
@@ -337,3 +337,53 @@ fn creator_verify_request(
|
||||
`devnet_metaplex_creator_unverify_request` fournit le parcours inverse. Les deux requêtes restent en simulation-only par défaut.
|
||||
|
||||
Le test générique accepte toujours `KB_DEVNET_METAPLEX_OPERATION_JSON`, mais rejette explicitement le placeholder documentaire `...`. Les opérations plus complexes recevront des préparateurs de fixtures dédiés au lieu d’exiger de longs JSON écrits à la main.
|
||||
## Solana Program Metadata
|
||||
|
||||
L’inventaire fermé est exposé par :
|
||||
|
||||
```rust
|
||||
fn program_metadata_scenarios(
|
||||
) -> Vec<kb_pipeline_demo_scenarios::SolanaProgramMetadataScenario> {
|
||||
kb_pipeline_demo_scenarios::solana_program_metadata_devnet_scenarios()
|
||||
}
|
||||
```
|
||||
|
||||
Les deux parcours sont ordonnés ainsi :
|
||||
|
||||
1. `Allocate → Extend → Write → Trim → Close` pour le `Buffer` ;
|
||||
2. `Initialize → SetData → SetAuthority → SetImmutable` pour le compte `Metadata`.
|
||||
|
||||
`Extend` précède volontairement `Write` : `Allocate` ne réserve que l’en-tête du Buffer, alors que l’écriture exige une capacité suffisante.
|
||||
|
||||
La préparation crée deux PDA non canoniques, les préfinance avec deux transferts System Program confirmés, puis produit neuf opérations typées avec leurs lectures, preuves de rent et postconditions. Elle exige un profil Devnet persistant, `devnet_send_enabled` et une confirmation opérateur explicite.
|
||||
|
||||
```rust
|
||||
async fn run_program_metadata_campaign<O>(
|
||||
pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<kb_pipeline_demo_scenarios::DevnetSolanaProgramMetadataCampaignSummary>
|
||||
where
|
||||
O: kb_pipeline_demo_scenarios::SolanaExecutionObserver,
|
||||
{
|
||||
let options = kb_pipeline_demo_scenarios::SolanaProgramMetadataFixturePreparationOptions {
|
||||
query_role: "http_queries".to_string(),
|
||||
transaction_role: "http_transactions".to_string(),
|
||||
operator_confirmed: true,
|
||||
};
|
||||
kb_pipeline_demo_scenarios::execute_devnet_solana_program_metadata_campaign(
|
||||
pool,
|
||||
profile,
|
||||
workspace_root,
|
||||
&options,
|
||||
observer,
|
||||
)
|
||||
.await
|
||||
}
|
||||
```
|
||||
|
||||
Cet appel crée des comptes et soumet onze transactions réelles au minimum : deux transferts de préfinancement et neuf opérations `ProgM6…`. Il ne doit jamais être déclenché implicitement par une ouverture de fenêtre ou une simple demande de simulation.
|
||||
|
||||
La matrice `SOLANA_PROGRAM_METADATA_DEVNET_VALIDATION_MATRIX.json` reste à `not_run` tant que les signatures, simulations, postconditions et preuves matérialisées n’ont pas été enregistrées. `Close` exige une preuve `account_absence`, car un compte absent ne produit pas de snapshot matérialisé.
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/constants.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Demo scenario constants.
|
||||
|
||||
/// Canonical tracing target for demo pipeline scenarios.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb-pipeline-demo-scenarios";
|
||||
|
||||
/// Total transactions reserved by one complete Solana Program Metadata campaign.
|
||||
pub(crate) const SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT: u64 = 11;
|
||||
/// Canonical tracing target for Solana Program Metadata Devnet fixtures.
|
||||
pub(crate) const TRACING_TARGET_METADATA_SOLANA_PROGRAM_FIXTURE: &str =
|
||||
"kb-pipeline-demo-scenarios.metadata.solana_program.fixture";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/lib.rs
|
||||
// version: 12
|
||||
// version: 13
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -9,6 +9,11 @@
|
||||
|
||||
mod constants;
|
||||
mod environment;
|
||||
mod metadata_solana_program_campaign;
|
||||
mod metadata_solana_program_devnet_execution;
|
||||
mod metadata_solana_program_fixture;
|
||||
mod metadata_solana_program_scenarios;
|
||||
mod metadata_solana_program_validation;
|
||||
mod metaplex_token_metadata_fixture;
|
||||
mod solana_ata_execution;
|
||||
mod solana_execution;
|
||||
@@ -31,6 +36,52 @@ pub use self::environment::initialize_demo_scenario_environment;
|
||||
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;
|
||||
/// One confirmed step retained by the complete Devnet campaign.
|
||||
pub use self::metadata_solana_program_campaign::DevnetSolanaProgramMetadataCampaignStepSummary;
|
||||
/// Complete evidence retained by the two-journey Devnet campaign.
|
||||
pub use self::metadata_solana_program_campaign::DevnetSolanaProgramMetadataCampaignSummary;
|
||||
/// Prepares fresh accounts and executes the nine stable operations on Devnet.
|
||||
pub use self::metadata_solana_program_campaign::execute_devnet_solana_program_metadata_campaign;
|
||||
/// Complete request for one Solana Program Metadata Devnet execution.
|
||||
pub use self::metadata_solana_program_devnet_execution::DevnetSolanaProgramMetadataExecutionRequest;
|
||||
/// Complete evidence produced by one Solana Program Metadata Devnet execution.
|
||||
pub use self::metadata_solana_program_devnet_execution::DevnetSolanaProgramMetadataExecutionSummary;
|
||||
/// Executes one Solana Program Metadata simulation or authorized submission.
|
||||
pub use self::metadata_solana_program_devnet_execution::execute_devnet_solana_program_metadata;
|
||||
/// Simulates one stable Solana Program Metadata operation against Devnet.
|
||||
pub use self::metadata_solana_program_devnet_execution::simulate_devnet_solana_program_metadata;
|
||||
/// Bytes reserved for the public Buffer journey before `Trim`.
|
||||
pub use self::metadata_solana_program_fixture::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES;
|
||||
/// Options used to prepare one complete Solana Program Metadata fixture.
|
||||
pub use self::metadata_solana_program_fixture::SolanaProgramMetadataFixturePreparationOptions;
|
||||
/// Public fixture values for the two Solana Program Metadata journeys.
|
||||
pub use self::metadata_solana_program_fixture::SolanaProgramMetadataFixturePreparationSummary;
|
||||
/// One prepared executable step in a Solana Program Metadata journey.
|
||||
pub use self::metadata_solana_program_fixture::SolanaProgramMetadataPreparedStep;
|
||||
/// Creates two unique pre-funded PDAs and the nine typed journey steps.
|
||||
pub use self::metadata_solana_program_fixture::prepare_solana_program_metadata_fixture;
|
||||
/// Stable lifecycle state consumed or produced by one scenario step.
|
||||
pub use self::metadata_solana_program_scenarios::SolanaProgramMetadataFixtureState;
|
||||
/// One ordered multi-step Solana Program Metadata journey.
|
||||
pub use self::metadata_solana_program_scenarios::SolanaProgramMetadataScenario;
|
||||
/// One stable step in a Solana Program Metadata Devnet journey.
|
||||
pub use self::metadata_solana_program_scenarios::SolanaProgramMetadataScenarioStep;
|
||||
/// Returns the two ordered Devnet journeys covering all nine stable operations.
|
||||
pub use self::metadata_solana_program_scenarios::solana_program_metadata_devnet_scenarios;
|
||||
/// Maximum evidence entries accepted by one operation validation row.
|
||||
pub use self::metadata_solana_program_validation::MAX_SOLANA_PROGRAM_METADATA_VALIDATION_EVIDENCE;
|
||||
/// One bounded evidence record.
|
||||
pub use self::metadata_solana_program_validation::SolanaProgramMetadataValidationEvidence;
|
||||
/// Closed validation matrix for all nine stable operations.
|
||||
pub use self::metadata_solana_program_validation::SolanaProgramMetadataValidationMatrix;
|
||||
/// One operation row in the closed Devnet validation matrix.
|
||||
pub use self::metadata_solana_program_validation::SolanaProgramMetadataValidationOperation;
|
||||
/// Exact network validation status.
|
||||
pub use self::metadata_solana_program_validation::SolanaProgramMetadataValidationStatus;
|
||||
/// Loads and validates the canonical Solana Program Metadata Devnet matrix.
|
||||
pub use self::metadata_solana_program_validation::load_solana_program_metadata_validation_matrix;
|
||||
/// Validates exact inventory, order and conservative evidence claims.
|
||||
pub use self::metadata_solana_program_validation::validate_solana_program_metadata_validation_matrix;
|
||||
/// Options for one Metaplex Create fixture preparation.
|
||||
pub use self::metaplex_token_metadata_fixture::MetaplexCreateFixturePreparationOptions;
|
||||
/// Public values generated for one Metaplex Create fixture.
|
||||
@@ -226,5 +277,9 @@ 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;
|
||||
|
||||
/// Total transactions reserved by one complete Solana Program Metadata campaign.
|
||||
pub(crate) use self::constants::SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT;
|
||||
/// Canonical tracing target for demo pipeline scenarios.
|
||||
pub(crate) use self::constants::TRACING_TARGET;
|
||||
/// Canonical tracing target for Solana Program Metadata Devnet fixtures.
|
||||
pub(crate) use self::constants::TRACING_TARGET_METADATA_SOLANA_PROGRAM_FIXTURE;
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_campaign.rs
|
||||
// version: 1
|
||||
|
||||
//! Ordered confirmed Devnet campaign for all Solana Program Metadata operations.
|
||||
|
||||
/// One confirmed step retained by the complete Devnet campaign.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetSolanaProgramMetadataCampaignStepSummary {
|
||||
/// Stable scenario identifier.
|
||||
pub scenario_id: std::string::String,
|
||||
/// Stable scenario-step identifier.
|
||||
pub step_id: std::string::String,
|
||||
/// Zero-based position inside the scenario.
|
||||
pub step_index: usize,
|
||||
/// Complete simulation, submission and stateful evidence.
|
||||
pub execution: crate::DevnetSolanaProgramMetadataExecutionSummary,
|
||||
}
|
||||
|
||||
/// Complete evidence retained by the two-journey Devnet campaign.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetSolanaProgramMetadataCampaignSummary {
|
||||
/// Fresh pre-funded fixture used by the campaign.
|
||||
pub fixture: crate::SolanaProgramMetadataFixturePreparationSummary,
|
||||
/// Nine confirmed steps in deterministic journey order.
|
||||
pub steps: std::vec::Vec<crate::DevnetSolanaProgramMetadataCampaignStepSummary>,
|
||||
}
|
||||
|
||||
/// Prepares fresh accounts and executes the nine stable operations on Devnet.
|
||||
///
|
||||
/// The campaign always submits transactions. The caller must explicitly confirm
|
||||
/// both fixture prefunding and all subsequent state mutations.
|
||||
pub async fn execute_devnet_solana_program_metadata_campaign<O>(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DevnetSolanaProgramMetadataCampaignSummary>
|
||||
where
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
if !options.operator_confirmed {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata Devnet campaign requires explicit operator confirmation",
|
||||
));
|
||||
}
|
||||
let fixture = match crate::prepare_solana_program_metadata_fixture(
|
||||
http_pool,
|
||||
profile,
|
||||
workspace_root,
|
||||
options,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut steps = std::vec::Vec::with_capacity(fixture.prepared_steps.len());
|
||||
for prepared in &fixture.prepared_steps {
|
||||
let mut request = prepared
|
||||
.execution_request(format!("spm-devnet-{}-{}", prepared.scenario_id, prepared.step_id));
|
||||
request.query_role = options.query_role.clone();
|
||||
request.transaction_role = options.transaction_role.clone();
|
||||
request.submit = true;
|
||||
request.operator_confirmed = true;
|
||||
let execution = match crate::execute_devnet_solana_program_metadata(
|
||||
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),
|
||||
};
|
||||
let confirmed = match execution.confirmation.as_ref() {
|
||||
std::option::Option::Some(value) => matches!(
|
||||
value.status,
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||
),
|
||||
std::option::Option::None => false,
|
||||
};
|
||||
if !confirmed || execution.post_execution.is_none() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_campaign_step_not_confirmed",
|
||||
format!(
|
||||
"campaign step {} did not reach confirmed stateful completion",
|
||||
prepared.step_id
|
||||
),
|
||||
));
|
||||
}
|
||||
if prepared.operation.operation_code() != kb_lib::EX_METADATA_SPM_CLOSE_OPERATION
|
||||
&& execution.materialized_snapshots.is_empty()
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_campaign_snapshot_missing",
|
||||
format!(
|
||||
"campaign step {} did not produce its required materialized snapshot",
|
||||
prepared.step_id
|
||||
),
|
||||
));
|
||||
}
|
||||
steps.push(crate::DevnetSolanaProgramMetadataCampaignStepSummary {
|
||||
scenario_id: prepared.scenario_id.clone(),
|
||||
step_id: prepared.step_id.clone(),
|
||||
step_index: prepared.step_index,
|
||||
execution,
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(crate::DevnetSolanaProgramMetadataCampaignSummary {
|
||||
fixture,
|
||||
steps,
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn campaign_contract_requires_exactly_nine_ordered_steps() {
|
||||
let scenarios = crate::solana_program_metadata_devnet_scenarios();
|
||||
let step_ids = scenarios
|
||||
.iter()
|
||||
.flat_map(|scenario| return scenario.steps.iter())
|
||||
.map(|step| return step.id.as_str())
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(step_ids.len(), 9);
|
||||
assert_eq!(step_ids.first(), std::option::Option::Some(&"spm_buffer_allocate"));
|
||||
assert_eq!(step_ids.last(), std::option::Option::Some(&"spm_metadata_set_immutable"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,715 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_devnet_execution.rs
|
||||
// version: 1
|
||||
|
||||
//! Real Devnet simulation and controlled submission for Solana Program Metadata.
|
||||
|
||||
/// Complete request for one Solana Program Metadata Devnet execution.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetSolanaProgramMetadataExecutionRequest {
|
||||
/// 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 stable Solana Program Metadata operation.
|
||||
pub operation: kb_lib::ExMetadataSpmOperation,
|
||||
/// Bounded state reads required before execution.
|
||||
pub preflight_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||
/// Explicit rent observations required by allocation and growth operations.
|
||||
pub rent_observations: std::vec::Vec<kb_pipeline::SolanaProgramMetadataRentObservation>,
|
||||
/// Bounded state reads repeated after confirmed submission.
|
||||
pub postcondition_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||
/// Explicit approval for overwrite, destructive or irreversible operations.
|
||||
pub allow_destructive_operation: bool,
|
||||
/// Explicitly authorizes signing and submission after successful simulation.
|
||||
pub submit: bool,
|
||||
/// Explicit operator confirmation required for submission.
|
||||
pub operator_confirmed: bool,
|
||||
/// Materializes confirmed account snapshots after submission.
|
||||
pub materialize_after_confirmation: bool,
|
||||
}
|
||||
|
||||
impl crate::DevnetSolanaProgramMetadataExecutionRequest {
|
||||
/// Creates a conservative simulation-only request.
|
||||
pub fn new(
|
||||
intent_id: impl std::convert::Into<std::string::String>,
|
||||
operation: kb_lib::ExMetadataSpmOperation,
|
||||
) -> 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(),
|
||||
rent_observations: std::vec::Vec::new(),
|
||||
postcondition_reads: std::vec::Vec::new(),
|
||||
allow_destructive_operation: false,
|
||||
submit: false,
|
||||
operator_confirmed: false,
|
||||
materialize_after_confirmation: false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates one conservative request from serialized typed operation JSON.
|
||||
pub fn from_operation_json(
|
||||
intent_id: impl std::convert::Into<std::string::String>,
|
||||
operation_json: &str,
|
||||
) -> kb_core::Result<Self> {
|
||||
if operation_json.contains('<') || operation_json.trim() == "..." {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata operation JSON still contains a placeholder",
|
||||
));
|
||||
}
|
||||
let operation = match serde_json::from_str::<kb_lib::ExMetadataSpmOperation>(operation_json)
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(format!(
|
||||
"invalid typed Solana Program Metadata operation JSON: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(Self::new(intent_id, operation));
|
||||
}
|
||||
|
||||
/// Validates request-local bounds independently from one profile.
|
||||
pub fn validate(&self) -> kb_core::Result<()> {
|
||||
if self.intent_id.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata Devnet 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(
|
||||
"Solana Program Metadata endpoint roles must not be empty",
|
||||
));
|
||||
}
|
||||
if self.preflight_reads.is_empty()
|
||||
|| self.preflight_reads.len()
|
||||
> kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS
|
||||
|| self.postcondition_reads.len()
|
||||
> kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_PREFLIGHT_ACCOUNTS
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata stateful read inventory is empty or above the compiled bound",
|
||||
));
|
||||
}
|
||||
if self.operation.requires_explicit_approval() && !self.allow_destructive_operation {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_metadata_spm_destructive_approval_required",
|
||||
"destructive Solana Program Metadata execution requires explicit approval",
|
||||
));
|
||||
}
|
||||
if self.submit && self.postcondition_reads.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"submitted Solana Program Metadata execution requires postcondition reads",
|
||||
));
|
||||
}
|
||||
if self.materialize_after_confirmation && self.postcondition_reads.is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata materialization requires postcondition reads",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete evidence produced by one Solana Program Metadata Devnet execution.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct DevnetSolanaProgramMetadataExecutionSummary {
|
||||
/// 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::SolanaProgramMetadataStatefulReadResult>,
|
||||
/// Stateful preflight report bound to the exact plan.
|
||||
pub stateful_preflight: kb_pipeline::SolanaProgramMetadataPreflightReport,
|
||||
/// Exact prepared 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::SolanaProgramMetadataExecutionReadinessReport,
|
||||
/// 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::SolanaProgramMetadataStatefulReadResult>,
|
||||
/// Stateful postcondition report after confirmed submission.
|
||||
pub post_execution: std::option::Option<kb_pipeline::SolanaProgramMetadataPostExecutionReport>,
|
||||
/// Canonical projections emitted from confirmed account snapshots.
|
||||
pub materialized_snapshots: std::vec::Vec<kb_lib::MtApiMaterializedOutput>,
|
||||
}
|
||||
|
||||
struct PreparedSolanaProgramMetadataExecution {
|
||||
wallet: kb_wallet::TemporaryWallet,
|
||||
unsigned: kb_lib::ExSolanaUnsignedTransaction,
|
||||
evidence: kb_lib::ExSolanaSimulationEvidence,
|
||||
summary: crate::DevnetSolanaProgramMetadataExecutionSummary,
|
||||
}
|
||||
|
||||
/// Simulates one stable Solana Program Metadata operation against Devnet.
|
||||
pub async fn simulate_devnet_solana_program_metadata<O>(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DevnetSolanaProgramMetadataExecutionSummary>
|
||||
where
|
||||
O: crate::SolanaExecutionObserver,
|
||||
{
|
||||
if request.submit {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"simulate_devnet_solana_program_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 Solana Program Metadata simulation or authorized submission.
|
||||
pub async fn execute_devnet_solana_program_metadata<O>(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<crate::DevnetSolanaProgramMetadataExecutionSummary>
|
||||
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),
|
||||
};
|
||||
let report = match kb_pipeline::inspect_solana_program_metadata_post_execution(
|
||||
&kb_pipeline::SolanaProgramMetadataPostExecutionRequest {
|
||||
operation: request.operation.clone(),
|
||||
before: summary.before.clone(),
|
||||
after: summary.after.clone(),
|
||||
},
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if report.status == kb_pipeline::SolanaProgramMetadataPostconditionStatus::Contradicted {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_metadata_spm_postcondition_contradicted",
|
||||
"confirmed Solana Program Metadata transaction contradicted its stateful postcondition",
|
||||
));
|
||||
}
|
||||
summary.post_execution = std::option::Option::Some(report);
|
||||
if request.materialize_after_confirmation {
|
||||
summary.materialized_snapshots =
|
||||
materialize_confirmed_snapshots(summary.after.as_slice());
|
||||
}
|
||||
}
|
||||
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::DevnetSolanaProgramMetadataExecutionRequest,
|
||||
observer: &O,
|
||||
) -> kb_core::Result<PreparedSolanaProgramMetadataExecution>
|
||||
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, "solana_program_metadata_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 intent = build_intent(profile, request, fee_payer.clone());
|
||||
let plan = match kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&kb_lib::ExMetadataSolanaProgramMetadataExecutor,
|
||||
&intent,
|
||||
) {
|
||||
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_metadata_spm_plan_denied",
|
||||
crate::violation_message(plan_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
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_solana_program_metadata_preflight(
|
||||
&kb_pipeline::SolanaProgramMetadataPreflightRequest {
|
||||
intent: intent.clone(),
|
||||
plan: plan.clone(),
|
||||
before: before.clone(),
|
||||
rent_observations: request.rent_observations.clone(),
|
||||
},
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
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,
|
||||
"solana_program_metadata_simulation",
|
||||
format!("simulating exact Solana Program Metadata 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_solana_program_metadata_execution_readiness(
|
||||
&kb_pipeline::SolanaProgramMetadataExecutionReadinessRequest {
|
||||
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) => {
|
||||
let simulation_json = match serde_json::to_string_pretty(&simulation) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => "<simulation serialization unavailable>".to_string(),
|
||||
};
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
error.code(),
|
||||
format!("{}; simulation={simulation_json}", error.message()),
|
||||
));
|
||||
},
|
||||
};
|
||||
let evidence = unsigned.bind_simulation(simulation.clone());
|
||||
return std::result::Result::Ok(PreparedSolanaProgramMetadataExecution {
|
||||
wallet,
|
||||
unsigned,
|
||||
evidence,
|
||||
summary: crate::DevnetSolanaProgramMetadataExecutionSummary {
|
||||
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(),
|
||||
post_execution: std::option::Option::None,
|
||||
materialized_snapshots: std::vec::Vec::new(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn build_intent(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
|
||||
fee_payer: kb_lib::MdPubkey,
|
||||
) -> kb_lib::ExMetadataSpmExecutionIntent {
|
||||
return kb_lib::ExMetadataSpmExecutionIntent {
|
||||
intent_id: request.intent_id.clone(),
|
||||
fee_payer: fee_payer.clone(),
|
||||
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: true,
|
||||
core_extraction_required: true,
|
||||
decode_replay_required: true,
|
||||
materialization_required: true,
|
||||
},
|
||||
},
|
||||
allow_destructive_operation: request.allow_destructive_operation,
|
||||
operation: request.operation.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
async fn read_snapshots(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
requests: &[kb_pipeline::SolanaProgramMetadataStatefulReadRequest],
|
||||
) -> kb_core::Result<std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadResult>> {
|
||||
let mut results = std::vec::Vec::with_capacity(requests.len());
|
||||
for request in requests {
|
||||
let value =
|
||||
match kb_pipeline::read_solana_program_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 materialize_confirmed_snapshots(
|
||||
snapshots: &[kb_pipeline::SolanaProgramMetadataStatefulReadResult],
|
||||
) -> std::vec::Vec<kb_lib::MtApiMaterializedOutput> {
|
||||
return snapshots
|
||||
.iter()
|
||||
.filter_map(|result| return result.materialized_output.clone())
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn validate_profile(
|
||||
profile: &kb_config::ProfileConfig,
|
||||
request: &crate::DevnetSolanaProgramMetadataExecutionRequest,
|
||||
) -> kb_core::Result<()> {
|
||||
if profile.wallet.cluster != "devnet" {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata 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(
|
||||
"Solana Program Metadata orchestration requires an enabled persistent temporary wallet",
|
||||
));
|
||||
}
|
||||
if !profile.execution.require_simulation {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata 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(
|
||||
"Solana Program Metadata 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_metadata_spm_external_signer_unavailable",
|
||||
format!(
|
||||
"the Devnet Solana Program Metadata orchestrator can sign only with profile wallet {}; required signers are {}",
|
||||
wallet_pubkey,
|
||||
required_signers.join(",")
|
||||
),
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn wallet() -> kb_lib::MdPubkey {
|
||||
return kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([7_u8; 32]).to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_rejects_destructive_operations_without_approval() {
|
||||
let wallet = wallet();
|
||||
let mut request = crate::DevnetSolanaProgramMetadataExecutionRequest::new(
|
||||
"write",
|
||||
kb_lib::ExMetadataSpmOperation::Write {
|
||||
buffer: wallet.clone(),
|
||||
authority: wallet,
|
||||
offset: 0,
|
||||
source: kb_lib::ExMetadataSpmWriteSource::Inline { data: vec![1] },
|
||||
},
|
||||
);
|
||||
request
|
||||
.preflight_reads
|
||||
.push(kb_pipeline::SolanaProgramMetadataStatefulReadRequest {
|
||||
query_role: "http_queries".to_string(),
|
||||
account: request.operation_target_for_test(),
|
||||
expected_state: kb_pipeline::SolanaProgramMetadataExpectedAccountState::Buffer,
|
||||
min_context_slot: std::option::Option::None,
|
||||
max_data_bytes: kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
|
||||
});
|
||||
assert!(request.validate().is_err());
|
||||
request.allow_destructive_operation = true;
|
||||
assert!(request.validate().is_ok());
|
||||
}
|
||||
|
||||
trait OperationTargetForTest {
|
||||
fn operation_target_for_test(&self) -> kb_lib::MdPubkey;
|
||||
}
|
||||
|
||||
impl OperationTargetForTest for crate::DevnetSolanaProgramMetadataExecutionRequest {
|
||||
fn operation_target_for_test(&self) -> kb_lib::MdPubkey {
|
||||
return match &self.operation {
|
||||
kb_lib::ExMetadataSpmOperation::Write { buffer, .. } => buffer.clone(),
|
||||
_ => kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,949 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_fixture.rs
|
||||
// version: 3
|
||||
|
||||
//! Native Solana Program Metadata fixture preparation for Devnet journeys.
|
||||
|
||||
/// Bytes reserved for the public Buffer journey before `Trim`.
|
||||
pub const SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES: u16 = 128;
|
||||
|
||||
/// Options used to prepare one complete Solana Program Metadata fixture.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SolanaProgramMetadataFixturePreparationOptions {
|
||||
/// Endpoint role used for account, rent, balance, blockhash and fee calls.
|
||||
pub query_role: std::string::String,
|
||||
/// Endpoint role used for simulation, submission and confirmation.
|
||||
pub transaction_role: std::string::String,
|
||||
/// Explicit operator confirmation for the two prefunding transfers.
|
||||
pub operator_confirmed: bool,
|
||||
}
|
||||
|
||||
impl crate::SolanaProgramMetadataFixturePreparationOptions {
|
||||
/// Creates conservative defaults matching the Devnet execution profiles.
|
||||
pub fn new() -> Self {
|
||||
return Self {
|
||||
query_role: "http_queries".to_string(),
|
||||
transaction_role: "http_transactions".to_string(),
|
||||
operator_confirmed: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for crate::SolanaProgramMetadataFixturePreparationOptions {
|
||||
fn default() -> Self {
|
||||
return Self::new();
|
||||
}
|
||||
}
|
||||
|
||||
/// One prepared executable step in a Solana Program Metadata journey.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SolanaProgramMetadataPreparedStep {
|
||||
/// Stable scenario identifier.
|
||||
pub scenario_id: std::string::String,
|
||||
/// Stable scenario-step identifier.
|
||||
pub step_id: std::string::String,
|
||||
/// Zero-based step position inside the scenario.
|
||||
pub step_index: usize,
|
||||
/// Exact typed operation.
|
||||
pub operation: kb_lib::ExMetadataSpmOperation,
|
||||
/// Bounded state reads required before execution.
|
||||
pub preflight_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||
/// Explicit rent observations required by the operation.
|
||||
pub rent_observations: std::vec::Vec<kb_pipeline::SolanaProgramMetadataRentObservation>,
|
||||
/// Bounded state reads required after confirmation.
|
||||
pub postcondition_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||
/// Whether explicit destructive approval is required.
|
||||
pub allow_destructive_operation: bool,
|
||||
}
|
||||
|
||||
impl crate::SolanaProgramMetadataPreparedStep {
|
||||
/// Builds one simulation-only execution request from the prepared fixture step.
|
||||
pub fn execution_request(
|
||||
&self,
|
||||
intent_id: impl std::convert::Into<std::string::String>,
|
||||
) -> crate::DevnetSolanaProgramMetadataExecutionRequest {
|
||||
let mut request = crate::DevnetSolanaProgramMetadataExecutionRequest::new(
|
||||
intent_id,
|
||||
self.operation.clone(),
|
||||
);
|
||||
request.preflight_reads = self.preflight_reads.clone();
|
||||
request.rent_observations = self.rent_observations.clone();
|
||||
request.postcondition_reads = self.postcondition_reads.clone();
|
||||
request.allow_destructive_operation = self.allow_destructive_operation;
|
||||
request.materialize_after_confirmation = true;
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
/// Public fixture values for the two Solana Program Metadata journeys.
|
||||
#[derive(Clone, Debug, PartialEq, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SolanaProgramMetadataFixturePreparationSummary {
|
||||
/// Profile used by the fixture preparation.
|
||||
pub profile_name: std::string::String,
|
||||
/// Profile-wallet authority.
|
||||
pub authority: std::string::String,
|
||||
/// Executable program described by the non-canonical metadata PDA.
|
||||
pub described_program: std::string::String,
|
||||
/// Exact 16-byte Buffer seed as UTF-8 text.
|
||||
pub buffer_seed: std::string::String,
|
||||
/// Non-canonical Buffer PDA.
|
||||
pub buffer: std::string::String,
|
||||
/// Lamports transferred to pre-fund the Buffer PDA.
|
||||
pub buffer_prefund_lamports: u64,
|
||||
/// Confirmed Buffer prefunding signature.
|
||||
pub buffer_prefund_signature: std::string::String,
|
||||
/// Exact 16-byte Metadata seed as UTF-8 text.
|
||||
pub metadata_seed: std::string::String,
|
||||
/// Non-canonical Metadata PDA.
|
||||
pub metadata: std::string::String,
|
||||
/// Lamports transferred to pre-fund the Metadata PDA.
|
||||
pub metadata_prefund_lamports: u64,
|
||||
/// Confirmed Metadata prefunding signature.
|
||||
pub metadata_prefund_signature: std::string::String,
|
||||
/// Ordered executable steps for both journeys.
|
||||
pub prepared_steps: std::vec::Vec<crate::SolanaProgramMetadataPreparedStep>,
|
||||
}
|
||||
|
||||
/// Creates two unique pre-funded PDAs and the nine typed journey steps.
|
||||
pub async fn prepare_solana_program_metadata_fixture(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
workspace_root: &std::path::Path,
|
||||
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
|
||||
) -> kb_core::Result<crate::SolanaProgramMetadataFixturePreparationSummary> {
|
||||
if options.query_role.trim().is_empty() || options.transaction_role.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata fixture endpoint roles must not be empty",
|
||||
));
|
||||
}
|
||||
if profile.wallet.cluster != "devnet"
|
||||
|| !profile.wallet.devnet_send_enabled
|
||||
|| !profile.wallet.temporary_wallet_enabled
|
||||
|| !profile.wallet.temporary_wallet_persist
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata fixture requires one persistent send-enabled Devnet profile",
|
||||
));
|
||||
}
|
||||
if !options.operator_confirmed {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata fixture prefunding requires explicit operator confirmation",
|
||||
));
|
||||
}
|
||||
let genesis = match http_pool.get_genesis_hash_for_role(options.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",
|
||||
"Solana Program Metadata fixture preparation requires a Devnet endpoint",
|
||||
));
|
||||
}
|
||||
let operator = 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 authority = kb_lib::MdPubkey(operator.public_key());
|
||||
let described_program = kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string());
|
||||
let unique = uuid::Uuid::new_v4().simple().to_string();
|
||||
let buffer_seed = format!("buf{}", &unique[0..13]);
|
||||
let metadata_seed = format!("met{}", &unique[13..26]);
|
||||
let buffer = match kb_lib::executor_metadata_solana_program_metadata_derive_non_canonical_pda(
|
||||
&described_program,
|
||||
&authority,
|
||||
buffer_seed.as_bytes(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let metadata = match kb_lib::executor_metadata_solana_program_metadata_derive_non_canonical_pda(
|
||||
&described_program,
|
||||
&authority,
|
||||
metadata_seed.as_bytes(),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = ensure_fixture_addresses_are_missing(
|
||||
http_pool,
|
||||
options,
|
||||
&[buffer.clone(), metadata.clone()],
|
||||
)
|
||||
.await
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let buffer_header = match u64::try_from(kb_lib::DC_METADATA_SPM_BUFFER_HEADER_BYTES) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(error.to_string()));
|
||||
},
|
||||
};
|
||||
let metadata_header = match u64::try_from(kb_lib::DC_METADATA_SPM_METADATA_HEADER_BYTES) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(error.to_string()));
|
||||
},
|
||||
};
|
||||
let buffer_target_space = buffer_header
|
||||
.saturating_add(u64::from(crate::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES));
|
||||
let initial_metadata_bytes = br#"{"name":"Khadhroony Program Metadata","version":1}"#.to_vec();
|
||||
let updated_metadata_bytes =
|
||||
br#"{"name":"Khadhroony Program Metadata","version":2,"validated":true}"#.to_vec();
|
||||
let initial_metadata_data_len = match u64::try_from(initial_metadata_bytes.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(error.to_string()));
|
||||
},
|
||||
};
|
||||
let updated_metadata_data_len = match u64::try_from(updated_metadata_bytes.len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::config(error.to_string()));
|
||||
},
|
||||
};
|
||||
let initial_metadata_space = metadata_header.saturating_add(initial_metadata_data_len);
|
||||
let updated_metadata_space = metadata_header.saturating_add(updated_metadata_data_len);
|
||||
let buffer_header_rent =
|
||||
match rent_for_space(http_pool, options.query_role.as_str(), buffer_header).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let buffer_target_rent =
|
||||
match rent_for_space(http_pool, options.query_role.as_str(), buffer_target_space).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let initial_metadata_rent = match rent_for_space(
|
||||
http_pool,
|
||||
options.query_role.as_str(),
|
||||
initial_metadata_space,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let updated_metadata_rent = match rent_for_space(
|
||||
http_pool,
|
||||
options.query_role.as_str(),
|
||||
updated_metadata_space,
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let buffer_prefund_lamports = buffer_target_rent.max(buffer_header_rent);
|
||||
let metadata_prefund_lamports = updated_metadata_rent.max(initial_metadata_rent);
|
||||
let total =
|
||||
match buffer_prefund_lamports
|
||||
.checked_add(metadata_prefund_lamports)
|
||||
.and_then(|value| {
|
||||
return value.checked_add(
|
||||
profile
|
||||
.execution
|
||||
.max_fee_lamports
|
||||
.saturating_mul(crate::SOLANA_PROGRAM_METADATA_CAMPAIGN_TRANSACTION_COUNT),
|
||||
);
|
||||
}) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::config(
|
||||
"Solana Program Metadata fixture funding requirement overflowed u64",
|
||||
));
|
||||
},
|
||||
};
|
||||
let balance = match http_pool
|
||||
.get_balance_for_role(
|
||||
options.query_role.as_str(),
|
||||
&authority,
|
||||
&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 < total {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_fixture_balance_insufficient",
|
||||
format!(
|
||||
"Devnet fixture wallet has {} lamports but requires at least {}; fund or airdrop the profile wallet first",
|
||||
balance.lamports, total
|
||||
),
|
||||
));
|
||||
}
|
||||
let buffer_prefund_signature = match submit_prefund_transfer(
|
||||
http_pool,
|
||||
profile,
|
||||
options,
|
||||
&operator,
|
||||
buffer.clone(),
|
||||
buffer_prefund_lamports,
|
||||
"buffer",
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let metadata_prefund_signature = match submit_prefund_transfer(
|
||||
http_pool,
|
||||
profile,
|
||||
options,
|
||||
&operator,
|
||||
metadata.clone(),
|
||||
metadata_prefund_lamports,
|
||||
"metadata",
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let prepared_steps = prepared_steps(
|
||||
options.query_role.as_str(),
|
||||
authority.clone(),
|
||||
described_program.clone(),
|
||||
buffer.clone(),
|
||||
buffer_seed.as_bytes(),
|
||||
metadata.clone(),
|
||||
metadata_seed.as_bytes(),
|
||||
initial_metadata_bytes,
|
||||
updated_metadata_bytes,
|
||||
buffer_prefund_lamports,
|
||||
buffer_header,
|
||||
buffer_header_rent,
|
||||
buffer_target_space,
|
||||
buffer_target_rent,
|
||||
metadata_prefund_lamports,
|
||||
initial_metadata_space,
|
||||
initial_metadata_rent,
|
||||
updated_metadata_space,
|
||||
updated_metadata_rent,
|
||||
);
|
||||
tracing::info!(
|
||||
target: crate::TRACING_TARGET_METADATA_SOLANA_PROGRAM_FIXTURE,
|
||||
action = "prepare_solana_program_metadata_fixture",
|
||||
profile = profile.name.as_str(),
|
||||
authority = authority.0.as_str(),
|
||||
buffer = buffer.0.as_str(),
|
||||
metadata = metadata.0.as_str(),
|
||||
"prepared pre-funded Solana Program Metadata Devnet fixture"
|
||||
);
|
||||
return std::result::Result::Ok(crate::SolanaProgramMetadataFixturePreparationSummary {
|
||||
profile_name: profile.name.clone(),
|
||||
authority: authority.0,
|
||||
described_program: described_program.0,
|
||||
buffer_seed,
|
||||
buffer: buffer.0,
|
||||
buffer_prefund_lamports,
|
||||
buffer_prefund_signature,
|
||||
metadata_seed,
|
||||
metadata: metadata.0,
|
||||
metadata_prefund_lamports,
|
||||
metadata_prefund_signature,
|
||||
prepared_steps,
|
||||
});
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepared_steps(
|
||||
query_role: &str,
|
||||
authority: kb_lib::MdPubkey,
|
||||
described_program: kb_lib::MdPubkey,
|
||||
buffer: kb_lib::MdPubkey,
|
||||
buffer_seed: &[u8],
|
||||
metadata: kb_lib::MdPubkey,
|
||||
metadata_seed: &[u8],
|
||||
initial_metadata_bytes: std::vec::Vec<u8>,
|
||||
updated_metadata_bytes: std::vec::Vec<u8>,
|
||||
buffer_observed_lamports: u64,
|
||||
buffer_header_space: u64,
|
||||
buffer_header_rent: u64,
|
||||
buffer_target_space: u64,
|
||||
buffer_target_rent: u64,
|
||||
metadata_observed_lamports: u64,
|
||||
initial_metadata_space: u64,
|
||||
initial_metadata_rent: u64,
|
||||
updated_metadata_space: u64,
|
||||
updated_metadata_rent: u64,
|
||||
) -> std::vec::Vec<crate::SolanaProgramMetadataPreparedStep> {
|
||||
let context = kb_lib::ExMetadataSpmProgramContext {
|
||||
program: described_program.clone(),
|
||||
program_data: std::option::Option::None,
|
||||
};
|
||||
let buffer_vacant = read(
|
||||
query_role,
|
||||
buffer.clone(),
|
||||
kb_pipeline::SolanaProgramMetadataExpectedAccountState::Vacant,
|
||||
);
|
||||
let buffer_state = read(
|
||||
query_role,
|
||||
buffer.clone(),
|
||||
kb_pipeline::SolanaProgramMetadataExpectedAccountState::Buffer,
|
||||
);
|
||||
let buffer_missing = read(
|
||||
query_role,
|
||||
buffer.clone(),
|
||||
kb_pipeline::SolanaProgramMetadataExpectedAccountState::Missing,
|
||||
);
|
||||
let metadata_vacant = read(
|
||||
query_role,
|
||||
metadata.clone(),
|
||||
kb_pipeline::SolanaProgramMetadataExpectedAccountState::Vacant,
|
||||
);
|
||||
let metadata_state = read(
|
||||
query_role,
|
||||
metadata.clone(),
|
||||
kb_pipeline::SolanaProgramMetadataExpectedAccountState::Metadata,
|
||||
);
|
||||
return vec![
|
||||
prepared_step(
|
||||
"spm_buffer_lifecycle",
|
||||
"spm_buffer_allocate",
|
||||
0,
|
||||
kb_lib::ExMetadataSpmOperation::Allocate {
|
||||
buffer: buffer.clone(),
|
||||
authority: authority.clone(),
|
||||
seed: std::option::Option::Some(buffer_seed.to_vec()),
|
||||
program_context: std::option::Option::Some(context.clone()),
|
||||
canonical: false,
|
||||
allocate_account: true,
|
||||
},
|
||||
vec![buffer_vacant],
|
||||
vec![rent(
|
||||
buffer.clone(),
|
||||
buffer_header_space,
|
||||
buffer_observed_lamports,
|
||||
buffer_header_rent,
|
||||
)],
|
||||
vec![buffer_state.clone()],
|
||||
false,
|
||||
),
|
||||
prepared_step(
|
||||
"spm_buffer_lifecycle",
|
||||
"spm_buffer_extend",
|
||||
1,
|
||||
kb_lib::ExMetadataSpmOperation::Extend {
|
||||
account: buffer.clone(),
|
||||
authority: authority.clone(),
|
||||
length: crate::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES,
|
||||
program_context: std::option::Option::None,
|
||||
},
|
||||
vec![buffer_state.clone()],
|
||||
vec![rent(
|
||||
buffer.clone(),
|
||||
buffer_target_space,
|
||||
buffer_observed_lamports,
|
||||
buffer_target_rent,
|
||||
)],
|
||||
vec![buffer_state.clone()],
|
||||
false,
|
||||
),
|
||||
prepared_step(
|
||||
"spm_buffer_lifecycle",
|
||||
"spm_buffer_write",
|
||||
2,
|
||||
kb_lib::ExMetadataSpmOperation::Write {
|
||||
buffer: buffer.clone(),
|
||||
authority: authority.clone(),
|
||||
offset: 0,
|
||||
source: kb_lib::ExMetadataSpmWriteSource::Inline {
|
||||
data: b"Khadhroony Solana Program Metadata Buffer fixture".to_vec(),
|
||||
},
|
||||
},
|
||||
vec![buffer_state.clone()],
|
||||
std::vec::Vec::new(),
|
||||
vec![buffer_state.clone()],
|
||||
true,
|
||||
),
|
||||
prepared_step(
|
||||
"spm_buffer_lifecycle",
|
||||
"spm_buffer_trim",
|
||||
3,
|
||||
kb_lib::ExMetadataSpmOperation::Trim {
|
||||
account: buffer.clone(),
|
||||
authority: authority.clone(),
|
||||
destination: authority.clone(),
|
||||
program_context: std::option::Option::None,
|
||||
},
|
||||
vec![buffer_state.clone()],
|
||||
std::vec::Vec::new(),
|
||||
vec![buffer_state.clone()],
|
||||
true,
|
||||
),
|
||||
prepared_step(
|
||||
"spm_buffer_lifecycle",
|
||||
"spm_buffer_close",
|
||||
4,
|
||||
kb_lib::ExMetadataSpmOperation::Close {
|
||||
account: buffer.clone(),
|
||||
authority: authority.clone(),
|
||||
destination: authority.clone(),
|
||||
program_context: std::option::Option::None,
|
||||
},
|
||||
vec![buffer_state.clone()],
|
||||
std::vec::Vec::new(),
|
||||
vec![buffer_missing],
|
||||
true,
|
||||
),
|
||||
prepared_step(
|
||||
"spm_metadata_lifecycle",
|
||||
"spm_metadata_initialize",
|
||||
0,
|
||||
kb_lib::ExMetadataSpmOperation::Initialize {
|
||||
metadata: metadata.clone(),
|
||||
authority: authority.clone(),
|
||||
program: described_program,
|
||||
program_data: std::option::Option::None,
|
||||
canonical: false,
|
||||
seed: metadata_seed.to_vec(),
|
||||
encoding: kb_lib::ExMetadataSpmEncoding::Utf8,
|
||||
compression: kb_lib::ExMetadataSpmCompression::None,
|
||||
format: kb_lib::ExMetadataSpmFormat::Json,
|
||||
data_source: kb_lib::ExMetadataSpmDataSource::Direct,
|
||||
data: std::option::Option::Some(kb_lib::ExMetadataSpmDataInput::Direct {
|
||||
bytes: initial_metadata_bytes,
|
||||
}),
|
||||
allocate_account: true,
|
||||
},
|
||||
vec![metadata_vacant],
|
||||
vec![rent(
|
||||
metadata.clone(),
|
||||
initial_metadata_space,
|
||||
metadata_observed_lamports,
|
||||
initial_metadata_rent,
|
||||
)],
|
||||
vec![metadata_state.clone()],
|
||||
false,
|
||||
),
|
||||
prepared_step(
|
||||
"spm_metadata_lifecycle",
|
||||
"spm_metadata_set_data",
|
||||
1,
|
||||
kb_lib::ExMetadataSpmOperation::SetData {
|
||||
metadata: metadata.clone(),
|
||||
authority: authority.clone(),
|
||||
encoding: kb_lib::ExMetadataSpmEncoding::Utf8,
|
||||
compression: kb_lib::ExMetadataSpmCompression::None,
|
||||
format: kb_lib::ExMetadataSpmFormat::Json,
|
||||
source: kb_lib::ExMetadataSpmSetDataSource::Inline {
|
||||
data: kb_lib::ExMetadataSpmDataInput::Direct { bytes: updated_metadata_bytes },
|
||||
},
|
||||
program_context: std::option::Option::None,
|
||||
},
|
||||
vec![metadata_state.clone()],
|
||||
vec![rent(
|
||||
metadata.clone(),
|
||||
updated_metadata_space,
|
||||
metadata_observed_lamports,
|
||||
updated_metadata_rent,
|
||||
)],
|
||||
vec![metadata_state.clone()],
|
||||
true,
|
||||
),
|
||||
prepared_step(
|
||||
"spm_metadata_lifecycle",
|
||||
"spm_metadata_set_authority",
|
||||
2,
|
||||
kb_lib::ExMetadataSpmOperation::SetAuthority {
|
||||
account: metadata.clone(),
|
||||
authority: authority.clone(),
|
||||
new_authority: std::option::Option::Some(authority.clone()),
|
||||
program_context: std::option::Option::None,
|
||||
},
|
||||
vec![metadata_state.clone()],
|
||||
std::vec::Vec::new(),
|
||||
vec![metadata_state.clone()],
|
||||
true,
|
||||
),
|
||||
prepared_step(
|
||||
"spm_metadata_lifecycle",
|
||||
"spm_metadata_set_immutable",
|
||||
3,
|
||||
kb_lib::ExMetadataSpmOperation::SetImmutable {
|
||||
metadata: metadata.clone(),
|
||||
authority,
|
||||
program_context: std::option::Option::None,
|
||||
},
|
||||
vec![metadata_state.clone()],
|
||||
std::vec::Vec::new(),
|
||||
vec![metadata_state],
|
||||
true,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
fn prepared_step(
|
||||
scenario_id: &str,
|
||||
step_id: &str,
|
||||
step_index: usize,
|
||||
operation: kb_lib::ExMetadataSpmOperation,
|
||||
preflight_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||
rent_observations: std::vec::Vec<kb_pipeline::SolanaProgramMetadataRentObservation>,
|
||||
postcondition_reads: std::vec::Vec<kb_pipeline::SolanaProgramMetadataStatefulReadRequest>,
|
||||
allow_destructive_operation: bool,
|
||||
) -> crate::SolanaProgramMetadataPreparedStep {
|
||||
return crate::SolanaProgramMetadataPreparedStep {
|
||||
scenario_id: scenario_id.to_string(),
|
||||
step_id: step_id.to_string(),
|
||||
step_index,
|
||||
operation,
|
||||
preflight_reads,
|
||||
rent_observations,
|
||||
postcondition_reads,
|
||||
allow_destructive_operation,
|
||||
};
|
||||
}
|
||||
|
||||
fn read(
|
||||
query_role: &str,
|
||||
account: kb_lib::MdPubkey,
|
||||
expected_state: kb_pipeline::SolanaProgramMetadataExpectedAccountState,
|
||||
) -> kb_pipeline::SolanaProgramMetadataStatefulReadRequest {
|
||||
return kb_pipeline::SolanaProgramMetadataStatefulReadRequest {
|
||||
query_role: query_role.to_string(),
|
||||
account,
|
||||
expected_state,
|
||||
min_context_slot: std::option::Option::None,
|
||||
max_data_bytes: kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
|
||||
};
|
||||
}
|
||||
|
||||
fn rent(
|
||||
account: kb_lib::MdPubkey,
|
||||
target_space: u64,
|
||||
observed_lamports: u64,
|
||||
required_lamports: u64,
|
||||
) -> kb_pipeline::SolanaProgramMetadataRentObservation {
|
||||
return kb_pipeline::SolanaProgramMetadataRentObservation {
|
||||
account,
|
||||
target_space,
|
||||
observed_lamports,
|
||||
required_lamports,
|
||||
};
|
||||
}
|
||||
|
||||
async fn ensure_fixture_addresses_are_missing(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
|
||||
accounts: &[kb_lib::MdPubkey],
|
||||
) -> kb_core::Result<()> {
|
||||
for account in accounts {
|
||||
let value = match http_pool
|
||||
.get_account_info_for_role(
|
||||
options.query_role.as_str(),
|
||||
account,
|
||||
&kb_onchain_transport::GetAccountInfoConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if value.account.is_some() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_fixture_address_collision",
|
||||
format!("generated fixture address {} already exists", account.0),
|
||||
));
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
async fn rent_for_space(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
query_role: &str,
|
||||
space: u64,
|
||||
) -> kb_core::Result<u64> {
|
||||
let result = match http_pool
|
||||
.get_minimum_balance_for_rent_exemption_for_role(
|
||||
query_role,
|
||||
space,
|
||||
&kb_onchain_transport::GetMinimumBalanceForRentExemptionConfig::confirmed(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(result.minimum_balance_lamports);
|
||||
}
|
||||
|
||||
async fn submit_prefund_transfer(
|
||||
http_pool: &kb_onchain_transport::HttpEndpointPool,
|
||||
profile: &kb_config::ProfileConfig,
|
||||
options: &crate::SolanaProgramMetadataFixturePreparationOptions,
|
||||
operator: &kb_wallet::TemporaryWallet,
|
||||
recipient: kb_lib::MdPubkey,
|
||||
lamports: u64,
|
||||
label: &str,
|
||||
) -> kb_core::Result<std::string::String> {
|
||||
let payer = kb_lib::MdPubkey(operator.public_key());
|
||||
let 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![payer.clone()],
|
||||
dry_run: false,
|
||||
post_execution_validation: kb_lib::ExApiPostExecutionValidationPolicy {
|
||||
canonical_insert_required: false,
|
||||
core_extraction_required: false,
|
||||
decode_replay_required: false,
|
||||
materialization_required: false,
|
||||
},
|
||||
};
|
||||
let intent = kb_lib::ExSolanaCoreExecutionIntent {
|
||||
intent_id: format!("spm-fixture-prefund-{label}-{}", recipient.0),
|
||||
fee_payer: payer.clone(),
|
||||
policy,
|
||||
operation: kb_lib::ExSolanaCoreOperation::SystemTransfer {
|
||||
from: payer,
|
||||
to: recipient,
|
||||
lamports,
|
||||
},
|
||||
};
|
||||
let plan = match kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||||
&kb_lib::ExSolanaCoreExecutor,
|
||||
&intent,
|
||||
) {
|
||||
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(
|
||||
"solana_program_metadata_fixture_prefund_plan_denied",
|
||||
crate::violation_message(plan_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let latest_blockhash = match http_pool
|
||||
.get_latest_blockhash_for_role(
|
||||
options.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),
|
||||
};
|
||||
let fee = match http_pool
|
||||
.get_fee_for_message_for_role(
|
||||
options.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),
|
||||
};
|
||||
let simulation_rpc = match http_pool
|
||||
.simulate_transaction_for_role(
|
||||
options.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),
|
||||
);
|
||||
if !simulation.success {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_fixture_prefund_simulation_failed",
|
||||
crate::simulation_failure_message(&simulation),
|
||||
));
|
||||
}
|
||||
let send_evaluation = match kb_lib::ExSafetyChecker.evaluate_send(&plan, &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(
|
||||
"solana_program_metadata_fixture_prefund_send_denied",
|
||||
crate::violation_message(send_evaluation.violations.as_slice()),
|
||||
));
|
||||
}
|
||||
let evidence = unsigned.bind_simulation(simulation.clone());
|
||||
let signed = match unsigned.sign_after_simulation(&evidence, &[operator.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 send_config = match kb_onchain_transport::SendTransactionConfig::from_execution_config(
|
||||
&profile.execution,
|
||||
std::option::Option::Some(latest_blockhash.context.slot),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
if let std::result::Result::Err(error) = http_pool
|
||||
.send_transaction_for_role(
|
||||
options.transaction_role.as_str(),
|
||||
signed.transaction_base64().as_str(),
|
||||
&signature,
|
||||
&send_config,
|
||||
)
|
||||
.await
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let confirmation_config =
|
||||
match kb_onchain_transport::ConfirmTransactionConfig::from_execution_config(
|
||||
&profile.execution,
|
||||
std::option::Option::Some(latest_blockhash.last_valid_block_height),
|
||||
std::option::Option::Some(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(
|
||||
options.transaction_role.as_str(),
|
||||
options.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),
|
||||
};
|
||||
if !matches!(
|
||||
confirmation.status,
|
||||
kb_lib::ExApiExecutionConfirmationStatus::Confirmed
|
||||
| kb_lib::ExApiExecutionConfirmationStatus::Finalized
|
||||
) {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_fixture_prefund_confirmation_failed",
|
||||
format!("prefunding transaction stopped at {:?}", confirmation.status),
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(signature.0);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn fixture_seed_shapes_and_journey_inventory_are_exact() {
|
||||
let unique = uuid::Uuid::nil().simple().to_string();
|
||||
let buffer_seed = format!("buf{}", &unique[0..13]);
|
||||
let metadata_seed = format!("met{}", &unique[13..26]);
|
||||
assert_eq!(buffer_seed.len(), kb_lib::DC_METADATA_SPM_SEED_BYTES);
|
||||
assert_eq!(metadata_seed.len(), kb_lib::DC_METADATA_SPM_SEED_BYTES);
|
||||
let authority =
|
||||
kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([1_u8; 32]).to_string());
|
||||
let buffer =
|
||||
kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([2_u8; 32]).to_string());
|
||||
let metadata =
|
||||
kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([3_u8; 32]).to_string());
|
||||
let steps = super::prepared_steps(
|
||||
"custom_queries",
|
||||
authority,
|
||||
kb_lib::MdPubkey(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()),
|
||||
buffer,
|
||||
buffer_seed.as_bytes(),
|
||||
metadata,
|
||||
metadata_seed.as_bytes(),
|
||||
br#"{"version":1}"#.to_vec(),
|
||||
br#"{"version":2}"#.to_vec(),
|
||||
1_000_000,
|
||||
96,
|
||||
1_000_000,
|
||||
224,
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
109,
|
||||
1_000_000,
|
||||
109,
|
||||
1_000_000,
|
||||
);
|
||||
let actual = steps
|
||||
.iter()
|
||||
.map(|step| return step.operation.operation_code())
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let scenarios = crate::solana_program_metadata_devnet_scenarios();
|
||||
let expected = scenarios
|
||||
.iter()
|
||||
.flat_map(|scenario| return scenario.steps.iter())
|
||||
.map(|step| return step.operation_code.as_str())
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(actual, expected);
|
||||
assert!(steps.iter().all(|step| {
|
||||
return step
|
||||
.preflight_reads
|
||||
.iter()
|
||||
.chain(step.postcondition_reads.iter())
|
||||
.all(|read| return read.query_role == "custom_queries");
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_scenarios.rs
|
||||
// version: 2
|
||||
|
||||
//! Coherent Solana Program Metadata journeys for Devnet validation.
|
||||
|
||||
/// Stable lifecycle state consumed or produced by one scenario step.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SolanaProgramMetadataFixtureState {
|
||||
/// Pre-funded but uninitialized PDA.
|
||||
Prefunded,
|
||||
/// Initialized Buffer account.
|
||||
Buffer,
|
||||
/// Initialized mutable Metadata account.
|
||||
MutableMetadata,
|
||||
/// Initialized immutable Metadata account.
|
||||
ImmutableMetadata,
|
||||
/// Closed account.
|
||||
Closed,
|
||||
}
|
||||
|
||||
/// One stable step in a Solana Program Metadata Devnet journey.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SolanaProgramMetadataScenarioStep {
|
||||
/// Stable scenario-step identifier.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible label.
|
||||
pub label: std::string::String,
|
||||
/// Stable executor operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Expected state before the operation.
|
||||
pub initial_state: crate::SolanaProgramMetadataFixtureState,
|
||||
/// Expected state after confirmed execution.
|
||||
pub resulting_state: crate::SolanaProgramMetadataFixtureState,
|
||||
/// Whether the operation requires explicit destructive approval.
|
||||
pub requires_explicit_approval: bool,
|
||||
/// Required evidence kinds.
|
||||
pub required_evidence: std::vec::Vec<std::string::String>,
|
||||
}
|
||||
|
||||
/// One ordered multi-step Solana Program Metadata journey.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SolanaProgramMetadataScenario {
|
||||
/// Stable scenario identifier.
|
||||
pub id: std::string::String,
|
||||
/// Operator-visible label.
|
||||
pub label: std::string::String,
|
||||
/// Fixture account selected by the journey.
|
||||
pub fixture_account: std::string::String,
|
||||
/// Ordered scenario steps.
|
||||
pub steps: std::vec::Vec<crate::SolanaProgramMetadataScenarioStep>,
|
||||
}
|
||||
|
||||
/// Returns the two ordered Devnet journeys covering all nine stable operations.
|
||||
pub fn solana_program_metadata_devnet_scenarios()
|
||||
-> std::vec::Vec<crate::SolanaProgramMetadataScenario> {
|
||||
return vec![
|
||||
crate::SolanaProgramMetadataScenario {
|
||||
id: "spm_buffer_lifecycle".to_string(),
|
||||
label: "Buffer : allocation, écriture, extension, trim et fermeture".to_string(),
|
||||
fixture_account: "buffer".to_string(),
|
||||
steps: vec![
|
||||
step(
|
||||
"spm_buffer_allocate",
|
||||
"Allouer le Buffer préfinancé",
|
||||
kb_lib::EX_METADATA_SPM_ALLOCATE_OPERATION,
|
||||
crate::SolanaProgramMetadataFixtureState::Prefunded,
|
||||
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||
false,
|
||||
),
|
||||
step(
|
||||
"spm_buffer_extend",
|
||||
"Étendre la capacité du Buffer",
|
||||
kb_lib::EX_METADATA_SPM_EXTEND_OPERATION,
|
||||
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||
false,
|
||||
),
|
||||
step(
|
||||
"spm_buffer_write",
|
||||
"Écrire les données inline",
|
||||
kb_lib::EX_METADATA_SPM_WRITE_OPERATION,
|
||||
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||
true,
|
||||
),
|
||||
step(
|
||||
"spm_buffer_trim",
|
||||
"Réduire le Buffer et restituer l’excédent",
|
||||
kb_lib::EX_METADATA_SPM_TRIM_OPERATION,
|
||||
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||
true,
|
||||
),
|
||||
step(
|
||||
"spm_buffer_close",
|
||||
"Fermer le Buffer",
|
||||
kb_lib::EX_METADATA_SPM_CLOSE_OPERATION,
|
||||
crate::SolanaProgramMetadataFixtureState::Buffer,
|
||||
crate::SolanaProgramMetadataFixtureState::Closed,
|
||||
true,
|
||||
),
|
||||
],
|
||||
},
|
||||
crate::SolanaProgramMetadataScenario {
|
||||
id: "spm_metadata_lifecycle".to_string(),
|
||||
label: "Metadata : initialisation, données, autorité et immutabilité".to_string(),
|
||||
fixture_account: "metadata".to_string(),
|
||||
steps: vec![
|
||||
step(
|
||||
"spm_metadata_initialize",
|
||||
"Initialiser les metadata non-canoniques",
|
||||
kb_lib::EX_METADATA_SPM_INITIALIZE_OPERATION,
|
||||
crate::SolanaProgramMetadataFixtureState::Prefunded,
|
||||
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||
false,
|
||||
),
|
||||
step(
|
||||
"spm_metadata_set_data",
|
||||
"Mettre à jour les données JSON",
|
||||
kb_lib::EX_METADATA_SPM_SET_DATA_OPERATION,
|
||||
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||
true,
|
||||
),
|
||||
step(
|
||||
"spm_metadata_set_authority",
|
||||
"Réaffirmer l’autorité du wallet de profil",
|
||||
kb_lib::EX_METADATA_SPM_SET_AUTHORITY_OPERATION,
|
||||
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||
true,
|
||||
),
|
||||
step(
|
||||
"spm_metadata_set_immutable",
|
||||
"Rendre les metadata immuables",
|
||||
kb_lib::EX_METADATA_SPM_SET_IMMUTABLE_OPERATION,
|
||||
crate::SolanaProgramMetadataFixtureState::MutableMetadata,
|
||||
crate::SolanaProgramMetadataFixtureState::ImmutableMetadata,
|
||||
true,
|
||||
),
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
fn step(
|
||||
id: &str,
|
||||
label: &str,
|
||||
operation_code: &str,
|
||||
initial_state: crate::SolanaProgramMetadataFixtureState,
|
||||
resulting_state: crate::SolanaProgramMetadataFixtureState,
|
||||
requires_explicit_approval: bool,
|
||||
) -> crate::SolanaProgramMetadataScenarioStep {
|
||||
return crate::SolanaProgramMetadataScenarioStep {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
operation_code: operation_code.to_string(),
|
||||
initial_state,
|
||||
resulting_state,
|
||||
requires_explicit_approval,
|
||||
required_evidence: required_evidence(resulting_state),
|
||||
};
|
||||
}
|
||||
|
||||
fn required_evidence(
|
||||
resulting_state: crate::SolanaProgramMetadataFixtureState,
|
||||
) -> std::vec::Vec<std::string::String> {
|
||||
let terminal_evidence = match resulting_state {
|
||||
crate::SolanaProgramMetadataFixtureState::Closed => "account_absence",
|
||||
_ => "materialized_snapshot",
|
||||
};
|
||||
return vec![
|
||||
"stateful_preflight".to_string(),
|
||||
"rpc_simulation".to_string(),
|
||||
"confirmed_signature".to_string(),
|
||||
"stateful_postcondition".to_string(),
|
||||
terminal_evidence.to_string(),
|
||||
];
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn journeys_cover_each_stable_operation_exactly_once() {
|
||||
let scenarios = crate::solana_program_metadata_devnet_scenarios();
|
||||
assert_eq!(scenarios.len(), 2);
|
||||
let operations = scenarios
|
||||
.iter()
|
||||
.flat_map(|scenario| return scenario.steps.iter())
|
||||
.map(|step| return step.operation_code.as_str())
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(operations.len(), kb_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES.len());
|
||||
for operation in kb_lib::EX_METADATA_SPM_SUPPORTED_OPERATION_CODES {
|
||||
assert_eq!(
|
||||
operations.iter().filter(|candidate| return **candidate == *operation).count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn destructive_flags_match_the_executor_contract() {
|
||||
for scenario in crate::solana_program_metadata_devnet_scenarios() {
|
||||
for step in scenario.steps {
|
||||
assert_eq!(
|
||||
step.requires_explicit_approval,
|
||||
kb_lib::EX_METADATA_SPM_DESTRUCTIVE_OPERATION_CODES
|
||||
.contains(&step.operation_code.as_str())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// file: kb-pipeline-demo-scenarios/src/metadata_solana_program_validation.rs
|
||||
// version: 2
|
||||
|
||||
//! Conservative Solana Program Metadata validation status and evidence contracts.
|
||||
|
||||
/// Maximum evidence entries accepted by one operation validation row.
|
||||
pub const MAX_SOLANA_PROGRAM_METADATA_VALIDATION_EVIDENCE: usize = 16;
|
||||
|
||||
/// Exact network validation status.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SolanaProgramMetadataValidationStatus {
|
||||
/// No network execution was attempted.
|
||||
NotRun,
|
||||
/// Exact transaction simulated successfully.
|
||||
Simulated,
|
||||
/// Transaction submitted but not yet confirmed.
|
||||
Submitted,
|
||||
/// Transaction confirmed and postconditions validated.
|
||||
Confirmed,
|
||||
/// Required network capability was unavailable.
|
||||
Unavailable,
|
||||
/// Validation failed with retained diagnostics.
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// One bounded evidence record.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SolanaProgramMetadataValidationEvidence {
|
||||
/// Stable evidence kind.
|
||||
pub kind: std::string::String,
|
||||
/// Non-secret evidence value.
|
||||
pub value: std::string::String,
|
||||
}
|
||||
|
||||
/// One operation row in the closed Devnet validation matrix.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SolanaProgramMetadataValidationOperation {
|
||||
/// Stable operation code.
|
||||
pub operation_code: std::string::String,
|
||||
/// Scenario step that exercises the operation.
|
||||
pub scenario_step_id: std::string::String,
|
||||
/// Exact current validation status.
|
||||
pub status: crate::SolanaProgramMetadataValidationStatus,
|
||||
/// Whether a destructive approval is required.
|
||||
pub requires_explicit_approval: bool,
|
||||
/// Evidence required for confirmed status.
|
||||
pub required_evidence: std::vec::Vec<std::string::String>,
|
||||
/// Bounded observed evidence.
|
||||
#[serde(default)]
|
||||
pub evidence: std::vec::Vec<crate::SolanaProgramMetadataValidationEvidence>,
|
||||
}
|
||||
|
||||
/// Closed validation matrix for all nine stable operations.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SolanaProgramMetadataValidationMatrix {
|
||||
/// Matrix schema version.
|
||||
pub matrix_version: u32,
|
||||
/// Exact owning prerelease.
|
||||
pub milestone: std::string::String,
|
||||
/// Ordered stable operations.
|
||||
pub operations: std::vec::Vec<crate::SolanaProgramMetadataValidationOperation>,
|
||||
}
|
||||
|
||||
/// Loads and validates the canonical Solana Program Metadata Devnet matrix.
|
||||
pub fn load_solana_program_metadata_validation_matrix()
|
||||
-> kb_core::Result<crate::SolanaProgramMetadataValidationMatrix> {
|
||||
let parsed = match serde_json::from_str::<crate::SolanaProgramMetadataValidationMatrix>(
|
||||
include_str!(
|
||||
"../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_DEVNET_VALIDATION_MATRIX.json"
|
||||
),
|
||||
) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_validation_matrix_invalid_json",
|
||||
format!("Solana Program Metadata validation matrix is invalid: {error}"),
|
||||
));
|
||||
},
|
||||
};
|
||||
if let std::result::Result::Err(error) =
|
||||
validate_solana_program_metadata_validation_matrix(&parsed)
|
||||
{
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return std::result::Result::Ok(parsed);
|
||||
}
|
||||
|
||||
/// Validates exact inventory, order and conservative evidence claims.
|
||||
pub fn validate_solana_program_metadata_validation_matrix(
|
||||
matrix: &crate::SolanaProgramMetadataValidationMatrix,
|
||||
) -> kb_core::Result<()> {
|
||||
if matrix.matrix_version != 1 || matrix.milestone != "0.4.8-pre.009" {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_validation_matrix_contract_mismatch",
|
||||
"Solana Program Metadata validation matrix must use version 1 for 0.4.8-pre.009",
|
||||
));
|
||||
}
|
||||
let compiled_steps = crate::solana_program_metadata_devnet_scenarios()
|
||||
.into_iter()
|
||||
.flat_map(|scenario| return scenario.steps)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let expected = compiled_steps
|
||||
.iter()
|
||||
.map(|step| return step.operation_code.as_str())
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let actual = matrix
|
||||
.operations
|
||||
.iter()
|
||||
.map(|operation| return operation.operation_code.as_str())
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
if actual != expected {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_validation_matrix_inventory_mismatch",
|
||||
"Solana Program Metadata validation matrix differs from the compiled journey order",
|
||||
));
|
||||
}
|
||||
for (operation, step) in matrix.operations.iter().zip(compiled_steps.iter()) {
|
||||
if operation.scenario_step_id != step.id
|
||||
|| operation.requires_explicit_approval != step.requires_explicit_approval
|
||||
|| operation.required_evidence != step.required_evidence
|
||||
|| operation.evidence.len() > crate::MAX_SOLANA_PROGRAM_METADATA_VALIDATION_EVIDENCE
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_validation_matrix_row_invalid",
|
||||
"Solana Program Metadata validation row differs from the compiled scenario contract",
|
||||
));
|
||||
}
|
||||
if matches!(
|
||||
operation.status,
|
||||
crate::SolanaProgramMetadataValidationStatus::NotRun
|
||||
| crate::SolanaProgramMetadataValidationStatus::Unavailable
|
||||
) && !operation.evidence.is_empty()
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_validation_unobserved_with_evidence",
|
||||
"not-run or unavailable validation rows must not contain observed evidence",
|
||||
));
|
||||
}
|
||||
if operation.status == crate::SolanaProgramMetadataValidationStatus::Confirmed {
|
||||
let observed = operation
|
||||
.evidence
|
||||
.iter()
|
||||
.map(|evidence| return evidence.kind.as_str())
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
if !operation
|
||||
.required_evidence
|
||||
.iter()
|
||||
.all(|kind| return observed.contains(kind.as_str()))
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"solana_program_metadata_validation_confirmed_without_evidence",
|
||||
"confirmed validation row lacks required evidence",
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn canonical_matrix_matches_the_compiled_two_journey_inventory() {
|
||||
let matrix = match crate::load_solana_program_metadata_validation_matrix() {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("matrix load failed: {error}"),
|
||||
};
|
||||
assert_eq!(matrix.operations.len(), 9);
|
||||
assert!(matrix.operations.iter().all(|operation| return operation.status
|
||||
== crate::SolanaProgramMetadataValidationStatus::NotRun));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// file: kb-pipeline-demo-scenarios/tests/external_metadata_solana_program_scenarios_api.rs
|
||||
// version: 2
|
||||
|
||||
//! Public API regression test for Solana Program Metadata Devnet scenarios.
|
||||
|
||||
#[test]
|
||||
fn external_consumers_use_only_public_solana_program_metadata_scenario_exports() {
|
||||
let scenarios = kb_pipeline_demo_scenarios::solana_program_metadata_devnet_scenarios();
|
||||
assert_eq!(scenarios.len(), 2);
|
||||
let matrix = match kb_pipeline_demo_scenarios::load_solana_program_metadata_validation_matrix()
|
||||
{
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
panic!("unable to load Solana Program Metadata validation matrix: {error}");
|
||||
},
|
||||
};
|
||||
assert_eq!(matrix.operations.len(), 9);
|
||||
let wallet = kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([3_u8; 32]).to_string());
|
||||
let operation = kb_lib::ExMetadataSpmOperation::Extend {
|
||||
account: wallet.clone(),
|
||||
authority: wallet.clone(),
|
||||
length: 1,
|
||||
program_context: std::option::Option::None,
|
||||
};
|
||||
let mut request = kb_pipeline_demo_scenarios::DevnetSolanaProgramMetadataExecutionRequest::new(
|
||||
"external-spm",
|
||||
operation.clone(),
|
||||
);
|
||||
request
|
||||
.preflight_reads
|
||||
.push(kb_pipeline::SolanaProgramMetadataStatefulReadRequest {
|
||||
query_role: "http_queries".to_string(),
|
||||
account: wallet.clone(),
|
||||
expected_state: kb_pipeline::SolanaProgramMetadataExpectedAccountState::Buffer,
|
||||
min_context_slot: std::option::Option::None,
|
||||
max_data_bytes: kb_pipeline::MAX_SOLANA_PROGRAM_METADATA_STATEFUL_ACCOUNT_BYTES,
|
||||
});
|
||||
assert!(request.validate().is_ok());
|
||||
let step = kb_pipeline_demo_scenarios::SolanaProgramMetadataPreparedStep {
|
||||
scenario_id: "external".to_string(),
|
||||
step_id: "extend".to_string(),
|
||||
step_index: 0,
|
||||
operation,
|
||||
preflight_reads: request.preflight_reads.clone(),
|
||||
rent_observations: std::vec::Vec::new(),
|
||||
postcondition_reads: std::vec::Vec::new(),
|
||||
allow_destructive_operation: false,
|
||||
};
|
||||
assert_eq!(step.execution_request("prepared").intent_id, "prepared");
|
||||
let fixture_options =
|
||||
kb_pipeline_demo_scenarios::SolanaProgramMetadataFixturePreparationOptions::new();
|
||||
assert_eq!(fixture_options.query_role, "http_queries");
|
||||
assert_eq!(
|
||||
kb_pipeline_demo_scenarios::SOLANA_PROGRAM_METADATA_FIXTURE_BUFFER_DATA_BYTES,
|
||||
128
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user