v0.4.8-pre.013

This commit is contained in:
2026-08-08 22:28:17 +02:00
parent f5efce576b
commit 8b831f8692
77 changed files with 15590 additions and 1585 deletions

View File

@@ -1,8 +1,29 @@
<!-- file: kb-pipeline/CHANGELOG.md -->
<!-- version: 22 -->
<!-- version: 28 -->
# CHANGELOG — kb-pipeline
## `0.4.8-pre.013`
- `pre.013-delta-fix-031` expose `MetaplexTokenMetadataAccountKind::TokenOwnedEscrow` dans les lectures stateful et réutilise le décodeur strict déjà présent dans `kb-lib`, y compris validation du propriétaire, du discriminant, du PDA dautorité et du bump ;
- ajoute une régression de projection canonique pour un escrow `TokenOwner` et conserve `base_token`, `authority_kind`, `creator` et `stored_bump` comme postconditions queryables.
- `pre.013-delta-fix-030` conserve les simulations Metaplex exactes échouées comme résultats observables lorsque `submit=false`, afin que les probes de disponibilité puissent classifier un refus runtime sans le transformer en erreur d'orchestration ;
- maintient une barrière stricte pour toute soumission : `submit=true` exige toujours `simulation_succeeded=true`, et une simulation négative ne peut jamais produire `send_authorized=true` ;
- ajoute une régression dédiée au contrat « failed simulation reportable for probe only ».
- `pre.013-delta-fix-021` ajoute une régression stateful construisant un compte `Edition` compact de 42 octets et exige sa matérialisation autoritative, afin de couvrir directement lallocation produite par un `Print` courant ;
- enrichit `metaplex_stateful_decode_failed` avec ladresse et le `kind` de la requête, sans affaiblir le refus des comptes invalides.
- `pre.013-delta-fix-020` réembarque explicitement le variant public `MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition }` afin quun overlay incomplet de `fix-019` ne puisse plus laisser le scénario `Print -> Burn` compiler contre un contrat stateful ancien ;
- ajoute un test dAPI externe qui construit et sérialise une requête `EditionMarker` uniquement via les exports crate-root ;
- valide également les pubkeys de dérivation `mint` et `token` directement dans `MetaplexTokenMetadataStatefulReadRequest::validate`.
- `pre.013-delta-fix-019` ajoute `MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition }` aux lectures stateful afin de décoder et matérialiser explicitement `EditionMarker`/`EditionMarkerV2` lors des campagnes déditions imprimées ;
- la projection stateful conserve le groupe, lindex de bit, le masque, `edition_taken`, le ledger borné et le payload canonique sans inventer de sémantique de fermeture après burn.
## `0.4.8-pre.013` — borne stateful Metaplex
- aligne `MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES` sur `kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES`, soit `65536` octets pour une lecture complète `getAccountInfo` ;
- valide la borne dans `MetaplexTokenMetadataStatefulReadRequest` avant la construction de la requête RPC ;
- conserve séparées les bornes de décodage offline et la borne du transport d'exécution ;
- ajoute une régression empêchant de réintroduire une limite stateful supérieure au contrat du transport.
## `0.4.8-pre.011` — validation Token-2022 Token Metadata
- ajoute lextraction bornée du retour base64 produit par `Emit` ;

View File

@@ -1,5 +1,5 @@
<!-- file: kb-pipeline/README.md -->
<!-- version: 8 -->
<!-- version: 9 -->
# kb-pipeline
@@ -18,7 +18,7 @@
- orchestration simulation-first, contrôles de readiness et postvalidation ;
- diagnostics stables exploitables par des applications ou scénarios.
Pour Metaplex Token Metadata, la crate expose les lectures stateful bornées, le préflight, la validation de readiness et lagrégation des postconditions.
Pour Metaplex Token Metadata, la crate expose les lectures stateful bornées, le préflight, la validation de readiness et lagrégation des postconditions. Une simulation exacte négative peut être conservée comme résultat observable uniquement lorsque `submit=false`; toute soumission continue dexiger une simulation réussie et ne peut jamais être autorisée à partir dun probe négatif.
Pour Solana Program Metadata, elle expose les mêmes frontières réutilisables pour les neuf opérations stables : lecture `Buffer`/`Metadata`, préflight stateful, gouvernance `ExSafetyChecker`, readiness simulation-first et postconditions. Elle ne contient ni fixture Devnet, ni wallet de démonstration, ni séquence métier spécifique à un test.

View File

@@ -1,8 +1,14 @@
<!-- file: kb-pipeline/USAGE.md -->
<!-- version: 10 -->
<!-- version: 16 -->
# Utilisation de kb-pipeline
## Lecture stateful des Edition Markers Metaplex
Les lectures stateful `Edition` acceptent les allocations Metaplex courantes après validation par `kb-lib`. Un `Print` récent produit notamment une allocation compacte de 42 octets alors que la structure Borsh `Edition` sérialise 41 octets ; le pipeline ne recalcule pas cette règle et consomme uniquement le snapshot déjà validé par le décodeur. Les erreurs de décodage stateful conservent ladresse et le `kind` demandés pour localiser la postcondition fautive.
Les campagnes déditions imprimées peuvent demander un snapshot borné `EditionMarker` en fournissant le mint maître et le numéro dédition. Le pipeline dérive et vérifie le contrat du compte via le décodeur Metaplex existant, puis expose notamment `marker_group`, `edition`, `byte_index`, `bit_mask`, `edition_taken` et le ledger. Cette lecture sert à prouver quun numéro dédition a été consommé ; elle ne suppose pas que le marker est fermé lors dun burn. La validation de requête contrôle aussi les pubkeys utilisées pour les dérivations `Edition`, `EditionMarker` et `TokenRecord` avant tout accès RPC.
## Objectif
La crate expose les campagnes bornées dacquisition, dextraction, de replay et les inspections stateful nécessaires aux applications et scénarios.
@@ -261,7 +267,9 @@ async fn read_metaplex_metadata(
}
```
Pour une edition, fournir le mint dans `MetaplexTokenMetadataAccountKind::Edition`. Pour un token record programmable, fournir le mint et le token account dans `TokenRecord`.
Pour une edition, fournir le mint dans `MetaplexTokenMetadataAccountKind::Edition`. Pour un token record programmable, fournir le mint et le token account dans `TokenRecord`. Pour un Token Owned Escrow, utiliser `TokenOwnedEscrow` : le décodeur récupère le mint parent et lautorité depuis le compte, puis valide lui-même le PDA et le bump avant projection stateful.
`MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES` représente la borne d'une lecture RPC complète et suit `kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES`, soit actuellement `65536` octets. Cette borne stateful ne doit pas être confondue avec les bornes propres aux décodeurs pour des octets déjà disponibles offline ou avec un futur lecteur par tranches.
## Inspecter le préflight Metaplex
@@ -326,6 +334,8 @@ fn validate_metaplex_execution(
Pour une soumission réelle, `submit` et `operator_confirmed` doivent être vrais et le plan ne doit plus être en `dry_run`.
Une requête `submit=false` peut conserver une simulation exacte ayant échoué afin quun appelant classe explicitement une indisponibilité runtime. Dans ce cas, le rapport reste non soumettable et contient le marqueur `failed_simulation_retained_for_probe_only`. Avec `submit=true`, une simulation échouée reste une erreur de readiness et `send_authorized` ne peut jamais devenir vrai.
## Agréger les postconditions Metaplex
```rust

View File

@@ -1,5 +1,5 @@
// file: kb-pipeline/src/metadata_metaplex_token_metadata_execution_orchestration.rs
// version: 2
// version: 3
//! Simulation-first Metaplex Token Metadata execution orchestration.
@@ -42,11 +42,13 @@ pub struct MetaplexTokenMetadataExecutionReadinessReport {
pub required_signers: std::vec::Vec<kb_lib::MdPubkey>,
/// Whether signing and submission are authorized.
pub send_authorized: bool,
/// Ordered successful checks.
/// Ordered readiness checks, including a probe-only failed-simulation marker when applicable.
pub checks: std::vec::Vec<std::string::String>,
}
/// Validates simulation, preflight, signer and submission policies.
/// Validates preflight, exact simulation evidence, signers and submission policies.
///
/// Failed simulations remain observable only for `submit=false`; submission always requires success.
pub fn validate_metaplex_token_metadata_execution_readiness(
request: &crate::MetaplexTokenMetadataExecutionReadinessRequest,
) -> kb_core::Result<crate::MetaplexTokenMetadataExecutionReadinessReport> {
@@ -59,11 +61,16 @@ pub fn validate_metaplex_token_metadata_execution_readiness(
if request.message_hash.trim().is_empty()
|| request.message_hash != request.simulated_message_hash
|| !request.simulated
|| !request.simulation_succeeded
{
return std::result::Result::Err(kb_core::Error::new(
"metaplex_execution_simulation_required",
"Metaplex execution requires successful exact-message simulation",
"Metaplex execution requires exact-message simulation evidence",
));
}
if request.submit && !request.simulation_succeeded {
return std::result::Result::Err(kb_core::Error::new(
"metaplex_execution_simulation_required",
"Metaplex submission requires successful exact-message simulation",
));
}
if request.resolved_signers.len() > crate::MAX_METAPLEX_TOKEN_METADATA_EXECUTION_SIGNERS {
@@ -108,10 +115,15 @@ pub fn validate_metaplex_token_metadata_execution_readiness(
required_signers: required.into_values().collect(),
send_authorized: request.submit
&& request.operator_confirmed
&& request.simulation_succeeded
&& !request.plan.policy.dry_run,
checks: vec![
"preflight_bound_to_operation".to_string(),
"simulation_bound_to_exact_message".to_string(),
if request.simulation_succeeded {
"simulation_bound_to_exact_message".to_string()
} else {
"failed_simulation_retained_for_probe_only".to_string()
},
"all_required_signers_resolved".to_string(),
"submission_policy_consistent".to_string(),
],
@@ -151,6 +163,64 @@ pub fn summarize_metaplex_token_metadata_postconditions(
#[cfg(test)]
mod tests {
#[test]
fn failed_simulation_is_reportable_only_without_submission() {
let fee_payer = kb_lib::MdPubkey("11111111111111111111111111111111".to_string());
let plan = kb_lib::ExApiPreparedExecutionPlan {
executor_name: "kb-lib.executor.metadata.metaplex_token_metadata".to_string(),
executor_version: "0.4.8".to_string(),
intent_id: "metaplex-failed-simulation-probe".to_string(),
operation_code: kb_lib::EX_METAPLEX_TOKEN_METADATA_USE_OPERATION.to_string(),
fee_payer: fee_payer.clone(),
instructions: vec![kb_lib::ExApiPlannedInstruction {
program_id: kb_lib::MdProgramId(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(),
),
operation_code: kb_lib::EX_METAPLEX_TOKEN_METADATA_USE_OPERATION.to_string(),
accounts: vec![],
data: vec![51],
}],
required_signers: vec![],
policy: kb_lib::ExApiExecutionPolicy {
dry_run: false,
..kb_lib::ExApiExecutionPolicy::default()
},
requested_spend_lamports: 0,
requested_compute_unit_price_micro_lamports: std::option::Option::None,
};
let request = crate::MetaplexTokenMetadataExecutionReadinessRequest {
plan: plan.clone(),
preflight: crate::MetaplexTokenMetadataPreflightReport {
operation_code: plan.operation_code.clone(),
context_slot: 42,
inspected_accounts: vec![],
checks: vec![],
},
message_hash: "message-hash".to_string(),
simulated_message_hash: "message-hash".to_string(),
simulated: true,
simulation_succeeded: false,
resolved_signers: vec![],
submit: false,
operator_confirmed: false,
};
let report = crate::validate_metaplex_token_metadata_execution_readiness(&request);
assert!(report.is_ok());
if let std::result::Result::Ok(report) = report {
assert!(!report.send_authorized);
assert!(
report
.checks
.iter()
.any(|check| return check == "failed_simulation_retained_for_probe_only")
);
}
let mut submission = request;
submission.submit = true;
submission.operator_confirmed = true;
assert!(crate::validate_metaplex_token_metadata_execution_readiness(&submission).is_err());
}
#[test]
fn postconditions_never_convert_not_applicable_into_success() {
assert_eq!(

View File

@@ -1,10 +1,14 @@
// file: kb-pipeline/src/metadata_metaplex_token_metadata_stateful.rs
// version: 4
// version: 9
//! Bounded Metaplex Token Metadata account reads and canonical state projections.
/// Maximum complete Metaplex account data accepted by one RPC read.
pub const MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES: usize = 1_048_576;
///
/// This bound follows the complete `getAccountInfo` contract of the execution transport. Decoder
/// limits remain independent for already-available offline bytes or future chunked readers.
pub const MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES: usize =
kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES;
/// Supported Metaplex account category for one bounded stateful read.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
@@ -17,6 +21,13 @@ pub enum MetaplexTokenMetadataAccountKind {
/// Mint used to derive the edition PDA.
mint: kb_lib::MdPubkey,
},
/// Edition marker PDA derived from the master mint and printed edition number.
EditionMarker {
/// Master mint used to derive the edition marker PDA.
mint: kb_lib::MdPubkey,
/// Printed edition number whose marker bit must be decoded.
edition: u64,
},
/// Programmable token record PDA derived from mint and token account.
TokenRecord {
/// Mint used to derive the token-record PDA.
@@ -24,6 +35,8 @@ pub enum MetaplexTokenMetadataAccountKind {
/// Token account used to derive the token-record PDA.
token: kb_lib::MdPubkey,
},
/// Token Owned Escrow PDA whose authority seeds and bump are self-validated by the decoder.
TokenOwnedEscrow,
}
/// One bounded Metaplex account read request.
@@ -41,6 +54,62 @@ pub struct MetaplexTokenMetadataStatefulReadRequest {
pub max_data_bytes: usize,
}
impl crate::MetaplexTokenMetadataStatefulReadRequest {
/// Validates the bounded complete-account RPC request.
pub fn validate(&self) -> kb_core::Result<()> {
if self.query_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex stateful read query_role must not be empty",
));
}
let account_result = kb_onchain_transport::validate_solana_pubkey_text(
self.account.0.as_str(),
"Metaplex stateful account",
);
if let std::result::Result::Err(error) = account_result {
return std::result::Result::Err(error);
}
let derivation_result = match &self.kind {
crate::MetaplexTokenMetadataAccountKind::Metadata => std::result::Result::Ok(()),
crate::MetaplexTokenMetadataAccountKind::Edition { mint }
| crate::MetaplexTokenMetadataAccountKind::EditionMarker { mint, .. } => {
kb_onchain_transport::validate_solana_pubkey_text(
mint.0.as_str(),
"Metaplex stateful derivation mint",
)
},
crate::MetaplexTokenMetadataAccountKind::TokenRecord { mint, token } => {
let mint_result = kb_onchain_transport::validate_solana_pubkey_text(
mint.0.as_str(),
"Metaplex stateful derivation mint",
);
if let std::result::Result::Err(error) = mint_result {
return std::result::Result::Err(error);
}
kb_onchain_transport::validate_solana_pubkey_text(
token.0.as_str(),
"Metaplex stateful derivation token",
)
},
crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow => {
std::result::Result::Ok(())
},
};
if let std::result::Result::Err(error) = derivation_result {
return std::result::Result::Err(error);
}
if self.max_data_bytes == 0
|| self.max_data_bytes > crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Metaplex stateful max_data_bytes must be between 1 and {}",
crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
)));
}
return std::result::Result::Ok(());
}
}
/// Canonical bounded Metaplex state snapshot.
#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct MetaplexTokenMetadataStatefulSnapshot {
@@ -72,10 +141,8 @@ pub async fn read_metaplex_token_metadata_stateful_snapshot(
pool: &kb_onchain_transport::HttpEndpointPool,
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
) -> kb_core::Result<crate::MetaplexTokenMetadataStatefulReadResult> {
if request.query_role.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::config(
"Metaplex stateful read query_role must not be empty",
));
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
let config = match kb_onchain_transport::GetAccountInfoConfig::new_with_data(
kb_onchain_transport::RpcCommitmentLevel::Confirmed,
@@ -100,13 +167,8 @@ pub fn materialize_metaplex_token_metadata_account_info_result(
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
result: &kb_onchain_transport::AccountInfoResult,
) -> kb_core::Result<crate::MetaplexTokenMetadataStatefulReadResult> {
if request.max_data_bytes == 0
|| request.max_data_bytes > crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
{
return std::result::Result::Err(kb_core::Error::config(format!(
"Metaplex stateful max_data_bytes must be between 1 and {}",
crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES
)));
if let std::result::Result::Err(error) = request.validate() {
return std::result::Result::Err(error);
}
if let std::option::Option::Some(minimum) = request.min_context_slot {
if result.context.slot < minimum {
@@ -154,6 +216,19 @@ pub fn materialize_metaplex_token_metadata_account_info_result(
});
}
fn stateful_decode_error(
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
error: kb_lib::DcMetadataMtmMetadataAccountDecodeError,
) -> kb_core::Error {
return kb_core::Error::new(
"metaplex_stateful_decode_failed",
format!(
"Metaplex stateful decode failed for account {} kind {:?}: {error}",
request.account.0, request.kind
),
);
}
fn decode_snapshot(
request: &crate::MetaplexTokenMetadataStatefulReadRequest,
slot: u64,
@@ -176,10 +251,9 @@ fn decode_snapshot(
payload_json: value.payload_json,
})
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
"metaplex_stateful_decode_failed",
error.to_string(),
)),
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::Edition { mint } => {
@@ -198,10 +272,46 @@ fn decode_snapshot(
payload_json: value.payload_json,
})
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
"metaplex_stateful_decode_failed",
error.to_string(),
)),
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition } => {
match kb_lib::decoder_metadata_metaplex_token_metadata_decode_edition_marker_account(
request.account.0.as_str(),
owner,
mint.0.as_str(),
*edition,
data,
) {
std::result::Result::Ok(value) => {
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
account: request.account.clone(),
account_kind: "edition_marker".to_string(),
mint: std::option::Option::Some(mint.clone()),
slot,
payload_json: serde_json::json!({
"account": value.account,
"bump": value.bump,
"mint": value.mint,
"kind": match value.kind {
kb_lib::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV1 => "edition_marker_v1",
kb_lib::DcMetadataMtmEditionMarkerAccountKind::EditionMarkerV2 => "edition_marker_v2",
},
"marker_group": value.marker_group,
"edition": value.edition,
"byte_index": value.byte_index,
"bit_mask": value.bit_mask,
"edition_taken": value.edition_taken,
"ledger": value.ledger,
"payload": value.payload_json,
}),
})
},
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::TokenRecord { mint, token } => {
@@ -221,10 +331,43 @@ fn decode_snapshot(
payload_json: value.payload_json,
})
},
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
"metaplex_stateful_decode_failed",
error.to_string(),
)),
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow => {
match kb_lib::decoder_metadata_metaplex_token_metadata_decode_token_owned_escrow_account(
request.account.0.as_str(),
owner,
data,
) {
std::result::Result::Ok(value) => {
let authority_kind = match value.authority_kind {
kb_lib::DcMetadataMtmTokenOwnedEscrowAuthorityKind::TokenOwner => {
"token_owner"
},
kb_lib::DcMetadataMtmTokenOwnedEscrowAuthorityKind::Creator => "creator",
};
std::result::Result::Ok(crate::MetaplexTokenMetadataStatefulSnapshot {
account: request.account.clone(),
account_kind: "token_owned_escrow".to_string(),
mint: std::option::Option::Some(kb_lib::MdPubkey(value.base_token.clone())),
slot,
payload_json: serde_json::json!({
"account": value.account,
"bump": value.bump,
"base_token": value.base_token,
"authority_kind": authority_kind,
"creator": value.creator,
"stored_bump": value.stored_bump,
"payload": value.payload_json,
}),
})
},
std::result::Result::Err(error) => {
std::result::Result::Err(stateful_decode_error(request, error))
},
}
},
};
@@ -252,6 +395,66 @@ mod tests {
std::result::Result::Err(error) => panic!("request deserialization failed: {error}"),
};
assert_eq!(decoded, request);
let marker = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::EditionMarker {
mint: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
edition: 1,
},
min_context_slot: std::option::Option::Some(43),
max_data_bytes: 1024,
};
let marker_json = match serde_json::to_string(&marker) {
std::result::Result::Ok(json) => json,
std::result::Result::Err(error) => {
panic!("marker request serialization failed: {error}")
},
};
let marker_decoded = match serde_json::from_str::<
crate::MetaplexTokenMetadataStatefulReadRequest,
>(marker_json.as_str())
{
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
panic!("marker request deserialization failed: {error}")
},
};
assert_eq!(marker_decoded, marker);
let escrow = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow,
min_context_slot: std::option::Option::Some(44),
max_data_bytes: 1024,
};
let escrow_json = match serde_json::to_string(&escrow) {
std::result::Result::Ok(json) => json,
std::result::Result::Err(error) => {
panic!("escrow request serialization failed: {error}")
},
};
let escrow_decoded = match serde_json::from_str::<
crate::MetaplexTokenMetadataStatefulReadRequest,
>(escrow_json.as_str())
{
std::result::Result::Ok(decoded) => decoded,
std::result::Result::Err(error) => {
panic!("escrow request deserialization failed: {error}")
},
};
assert_eq!(escrow_decoded, escrow);
let invalid_marker = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey("11111111111111111111111111111111".to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::EditionMarker {
mint: kb_lib::MdPubkey("invalid".to_string()),
edition: 1,
},
min_context_slot: std::option::Option::None,
max_data_bytes: 1024,
};
assert!(invalid_marker.validate().is_err());
}
#[test]
@@ -274,5 +477,124 @@ mod tests {
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result)
.is_err()
);
let mut oversized = request;
oversized.max_data_bytes = kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES + 1;
assert!(oversized.validate().is_err());
}
#[test]
fn token_owned_escrow_materializes_with_canonical_authority_projection() {
let program = match kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
.parse::<solana_pubkey::Pubkey>()
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
};
let mint = solana_pubkey::Pubkey::new_from_array([6_u8; 32]);
let (account, bump) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program.as_ref(), mint.as_ref(), &[0], b"escrow"],
&program,
);
let mut data = std::vec![10_u8];
data.extend_from_slice(mint.as_ref());
data.push(0);
data.push(bump);
assert_eq!(data.len(), 35);
let request = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey(account.to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::TokenOwnedEscrow,
min_context_slot: std::option::Option::Some(11),
max_data_bytes: 1024,
};
let result = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 11,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(kb_onchain_transport::AccountInfoValue {
lamports: 1_000,
owner: kb_lib::MdProgramId(program.to_string()),
executable: false,
rent_epoch: 0,
space: data.len() as u64,
data,
}),
};
let output =
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result);
let snapshot = match output {
std::result::Result::Ok(value) => value.snapshot,
std::result::Result::Err(error) => panic!("escrow stateful decode failed: {error}"),
};
assert_eq!(snapshot.account_kind, "token_owned_escrow");
assert_eq!(snapshot.mint, std::option::Option::Some(kb_lib::MdPubkey(mint.to_string())));
assert_eq!(snapshot.payload_json["base_token"], serde_json::json!(mint.to_string()));
assert_eq!(snapshot.payload_json["authority_kind"], serde_json::json!("token_owner"));
assert_eq!(snapshot.payload_json["creator"], serde_json::Value::Null);
assert_eq!(snapshot.payload_json["stored_bump"], serde_json::json!(bump));
}
#[test]
fn compact_printed_edition_account_materializes_from_current_network_allocation() {
let program = match kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID
.parse::<solana_pubkey::Pubkey>()
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("program parse failed: {error}"),
};
let mint = solana_pubkey::Pubkey::new_from_array([7_u8; 32]);
let parent = solana_pubkey::Pubkey::new_from_array([8_u8; 32]);
let (account, _) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program.as_ref(), mint.as_ref(), b"edition"],
&program,
);
let mut data = vec![1_u8];
data.extend_from_slice(parent.as_ref());
data.extend_from_slice(&1_u64.to_le_bytes());
data.push(0);
assert_eq!(data.len(), 42);
let request = crate::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account: kb_lib::MdPubkey(account.to_string()),
kind: crate::MetaplexTokenMetadataAccountKind::Edition {
mint: kb_lib::MdPubkey(mint.to_string()),
},
min_context_slot: std::option::Option::Some(12),
max_data_bytes: 1024,
};
let result = kb_onchain_transport::AccountInfoResult {
context: kb_onchain_transport::RpcResponseContext {
slot: 12,
api_version: std::option::Option::None,
},
account: std::option::Option::Some(kb_onchain_transport::AccountInfoValue {
lamports: 1_000,
owner: kb_lib::MdProgramId(program.to_string()),
executable: false,
rent_epoch: 0,
space: 42,
data,
}),
};
let output =
crate::materialize_metaplex_token_metadata_account_info_result(&request, &result);
let snapshot = match output {
std::result::Result::Ok(value) => value.snapshot,
std::result::Result::Err(error) => {
panic!("compact edition stateful decode failed: {error}")
},
};
assert_eq!(snapshot.account_kind, "edition");
assert_eq!(snapshot.payload_json["edition"], serde_json::json!(1));
assert_eq!(snapshot.payload_json["parent"], serde_json::json!(parent.to_string()));
}
#[test]
fn stateful_complete_read_bound_matches_the_transport_contract() {
assert_eq!(
crate::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
kb_onchain_transport::MAX_COMPLETE_ACCOUNT_DATA_BYTES
);
}
}

View File

@@ -0,0 +1,24 @@
// file: kb-pipeline/tests/external_metadata_metaplex_token_metadata_pipeline_api.rs
// version: 1
//! External-only coverage for the public Metaplex Token Metadata pipeline contract.
#[test]
fn external_consumers_can_request_edition_marker_state_through_crate_root_exports() {
let account = kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([7_u8; 32]).to_string());
let mint = kb_lib::MdPubkey(solana_pubkey::Pubkey::new_from_array([8_u8; 32]).to_string());
let request = kb_pipeline::MetaplexTokenMetadataStatefulReadRequest {
query_role: "rpc".to_string(),
account,
kind: kb_pipeline::MetaplexTokenMetadataAccountKind::EditionMarker { mint, edition: 1 },
min_context_slot: std::option::Option::Some(42),
max_data_bytes: kb_pipeline::MAX_METAPLEX_TOKEN_METADATA_ACCOUNT_BYTES,
};
assert!(request.validate().is_ok());
let json = match serde_json::to_value(&request) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("request serialization failed: {error}"),
};
assert_eq!(json["kind"]["kind"], "edition_marker");
assert_eq!(json["kind"]["edition"], 1);
}