v0.4.7-pre.003

This commit is contained in:
2026-08-02 09:04:23 +02:00
parent 7a50ddd531
commit 1bddeb834d
22 changed files with 1656 additions and 369 deletions

View File

@@ -1,8 +1,29 @@
<!-- file: kb-lib/CHANGELOG.md -->
<!-- version: 7 -->
<!-- version: 10 -->
# CHANGELOG — kb-lib
## 0.4.7-pre.003 — fondations de lexécuteur Metaplex
### Correctif `0.4.7-pre.003-delta-fix-003`
- ajout de tests unitaires dans `intent.rs`, `builder.rs` et `executor.rs` pour la surface Metaplex introduite en `pre.003` ;
- vérification des codes dopération, de la partition des opérations dépréciées, des politiques fail-closed, des PDA metadata/edition, du builder `PuffMetadata`, des capacités exactes et de la parité entre dispatch typé et générique.
### Correctif `0.4.7-pre.003-delta-fix-002`
- suppression de la dépendance directe inutile à `solana-program` dans le workspace et `kb-lib` ;
- utilisation des types modulaires `solana_instruction::Instruction` et `solana_pubkey::Pubkey` pour le contrat interne ;
- conversion locale des types de compatibilité retournés par `mpl-token-metadata 5.1.1`, sans exposer ni dépendre directement de `solana-program` ;
- suppression de tous les opérateurs `?` introduits par la surface Metaplex `pre.003` ;
- ajout des TODO de réaudit des exécuteurs Solana Core/SPL et de leur orchestration pipeline selon la nouvelle règle des opérations remplacées et obsolètes.
- remplacement de lexécuteur réservé par une surface typée initiale ;
- builders courants `Create` et `UpdateAsUpdateAuthorityV2` fondés sur `mpl-token-metadata` ;
- validation des PDA metadata/edition, des signers autorisés, des comptes de rule set et des plafonds de coût ;
- simulation et dry-run obligatoires pour cette première tranche ;
- correction de la classification des instructions remplacées et obsolètes.
## 0.4.7-pre.002 — contrat Metaplex fermé
- classification exhaustive des 58 discriminants Metaplex Token Metadata ;

View File

@@ -1,5 +1,5 @@
# file: kb-lib/Cargo.toml
# version: 14
# version: 15
[package]
name = "kb-lib"

View File

@@ -1,10 +1,12 @@
<!-- file: kb-lib/README.md -->
<!-- version: 15 -->
<!-- version: 16 -->
# kb-lib
`kb-lib` est la bibliothèque fonctionnelle consolidée de `khadhroony-bot3`. Elle expose les modèles partagés, les contrats et implémentations de décodage, de matérialisation, dexécution et de sécurité nécessaires au pipeline Solana.
- Metaplex Token Metadata expose les wrappers canoniques courants et conserve les opérations obsolètes encore constructibles sous une API Rust `#[deprecated]` avec approbation opérateur explicite ; les versions remplacées ne reçoivent quun renvoi vers leur remplacement canonique final.
## Périmètre
La crate regroupe quatre familles internes, exposées par la façade `kb_lib` :
@@ -56,7 +58,7 @@ Les matrices actives sont maintenues sous [`../test-fixtures/contract-matrices/`
## Statut et limites
La migration des surfaces historiques jusquà un périmètre proche de bot2 `0.4.6` est largement réalisée. Le décodeur Metaplex Token Metadata couvre les 58 discriminants et les matérialisations possèdent désormais un contrat fermé de propriété des faits. Lexécuteur et la clôture fonctionnelle de cette surface restent à réaliser entre `0.4.7-pre.003` et `pre.006`. Le registre ElGamal nest pas déclaré validé sur Devnet ou Mainnet.
La migration des surfaces historiques jusquà un périmètre proche de bot2 `0.4.6` est largement réalisée. Le décodeur Metaplex Token Metadata couvre les 58 discriminants et les matérialisations possèdent désormais un contrat fermé de propriété des faits. Lexécuteur Metaplex possède désormais ses fondations typées et les wrappers courants `Create`/`Update`; les autres opérations courantes restent à achever jusquà `pre.006`. Le registre ElGamal nest pas déclaré validé sur Devnet ou Mainnet.
## Documents

View File

@@ -1,8 +1,14 @@
<!-- file: kb-lib/TODO.md -->
<!-- version: 4 -->
<!-- version: 7 -->
# TODO — kb-lib
## `0.4.7` — Metaplex Token Metadata
- [x] établir le contrat fermé de couverture et de propriété des faits ;
- [x] introduire les intents et builders courants `Create`/`Update` ;
- [ ] achever Verify/Unverify, délégations, pNFT, éditions, use, escrow et opérations courantes restantes avant `pre.006` ;
## Entre `0.4.6` et `0.4.7` — Metaplex Token Metadata
- [x] Matérialisation - vérifier la couverture exacte migrée depuis bot2.
@@ -18,6 +24,14 @@
- [ ] Fixture - préparer une preuve `PubkeyValidity` et un compte `Proof Context State` valide si un réseau exploitable est confirmé.
- [ ] Validation - exécuter une validation réseau réelle avant de déclarer la surface validée.
## Réaudit ultérieur des exécuteurs déjà livrés
- [ ] Version à déterminer - réauditer les exécuteurs Solana Core et SPL déjà implémentés selon la règle distinguant les opérations courantes, remplacées et obsolètes constructibles.
- [ ] Solana Core - vérifier chaque génération historique ou obsolète officiellement constructible et ajouter les opérations dépréciées manquantes sans réexposer les versions remplacées intermédiaires.
- [ ] SPL Memo - réexaminer notamment Memo v1 et Memo v3 : conserver uniquement le remplacement canonique pour les versions remplacées et implémenter comme dépréciée toute génération obsolète encore légitimement constructible.
- [ ] SPL Token, ATA, Token-2022 et registre ElGamal - vérifier la même règle opération par opération, documenter les remplacements canoniques et compléter les builders dépréciés manquants.
- [ ] Matrices - aligner les statuts, annotations `#[deprecated]`, approbations opérateur et tests de parité de toutes les surfaces concernées.
## Versions ultérieures
- [ ] Surfaces - remplacer les décodeurs et exécuteurs réservés selon le ROADMAP.

View File

@@ -1,5 +1,5 @@
<!-- file: kb-lib/USAGE.md -->
<!-- version: 3 -->
<!-- version: 5 -->
# Utilisation de kb-lib
@@ -117,6 +117,7 @@ println!("edition parent={:?}", edition.parent);
Les signatures exactes doivent être vérifiées lors de lutilisation : certaines familles exigent un contexte ou une identité canonique supplémentaire.
## Exécution typée
`ExApiTypedInstructionExecutor` produit un `ExApiPreparedExecutionPlan` à partir dune intention et dune politique explicites. Les exécuteurs concrets incluent notamment :

View File

@@ -3067,9 +3067,7 @@ mod tests {
assert_eq!(entries.len(), 58);
let mut discriminators = std::collections::BTreeSet::new();
for entry in entries {
let discriminator = match entry
.get("discriminator")
.and_then(serde_json::Value::as_u64)
let discriminator = match entry.get("discriminator").and_then(serde_json::Value::as_u64)
{
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("instruction discriminator missing"),
@@ -3077,14 +3075,18 @@ mod tests {
assert!(discriminators.insert(discriminator));
assert!(entry.get("accounts").is_some_and(serde_json::Value::is_array));
assert!(entry.get("signers").is_some_and(serde_json::Value::is_array));
assert!(entry
.get("coverage_status")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| return value != "pending" && !value.is_empty()));
assert!(entry
.get("execute")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| return value != "pending" && !value.is_empty()));
assert!(
entry
.get("coverage_status")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| return value != "pending" && !value.is_empty())
);
assert!(
entry
.get("execute")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| return value != "pending" && !value.is_empty())
);
}
assert_eq!(discriminators, (0u64..58u64).collect());
}
@@ -3095,13 +3097,11 @@ mod tests {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("matrix parse failed: {error}"),
};
let facts = match matrix
.get("fact_ownership_contract")
.and_then(serde_json::Value::as_array)
{
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("fact_ownership_contract missing"),
};
let facts =
match matrix.get("fact_ownership_contract").and_then(serde_json::Value::as_array) {
std::option::Option::Some(value) => value,
std::option::Option::None => panic!("fact_ownership_contract missing"),
};
let mut names = std::collections::BTreeSet::new();
for entry in facts {
let fact = match entry.get("fact").and_then(serde_json::Value::as_str) {
@@ -3109,10 +3109,12 @@ mod tests {
std::option::Option::None => panic!("fact name missing"),
};
assert!(names.insert(fact));
assert!(entry
.get("owner")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| return !value.is_empty()));
assert!(
entry
.get("owner")
.and_then(serde_json::Value::as_str)
.is_some_and(|value| return !value.is_empty())
);
}
let forbidden = match matrix
.pointer("/fact_ownership_proof/cross_domain_merge_forbidden")

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/executor.rs
// version: 9
// version: 11
//! Consolidated executor modules.
@@ -208,12 +208,28 @@ pub use self::lending::ExLendingKaminoExecutor;
pub use self::lending::ExLendingMarginfiV2Executor;
/// Exposes the reserved `lock/raydium_lp` executor.
pub use self::lock::ExLockRaydiumLpExecutor;
/// Exposes the reserved `metadata/metaplex_token_metadata` executor.
/// Stable operation code for the current Metaplex Create wrapper.
pub use self::metadata::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION;
/// Deprecated Metaplex operation codes requiring explicit approval.
pub use self::metadata::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES;
/// Stable deprecated Metaplex PuffMetadata operation code.
pub use self::metadata::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION;
/// Stable deprecated Metaplex SetTokenStandard operation code.
pub use self::metadata::EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION;
/// Current and deprecated Metaplex Token Metadata operations.
pub use self::metadata::EX_METAPLEX_TOKEN_METADATA_SUPPORTED_OPERATION_CODES;
/// Stable operation code for the current Metaplex Update wrapper.
pub use self::metadata::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION;
/// Exposes the Metaplex Token Metadata executor.
pub use self::metadata::ExMetadataMetaplexTokenMetadataExecutor;
/// Reserved `kb-lib.executor.metadata.solana_program_metadata` executor.
pub use self::metadata::ExMetadataSolanaProgramMetadataExecutor;
/// Exposes the reserved `metadata/spl_name_service` executor.
pub use self::metadata::ExMetadataSplNameServiceExecutor;
/// Exposes the complete Metaplex Token Metadata execution intent.
pub use self::metadata::ExMetaplexTokenMetadataExecutionIntent;
/// Exposes one current Metaplex Token Metadata operation.
pub use self::metadata::ExMetaplexTokenMetadataOperation;
/// Exposes the reserved `nft/metaplex_bubblegum` executor.
pub use self::nft::ExNftMetaplexBubblegumExecutor;
/// Reserved `kb-lib.executor.nft.tensor_cnft` executor.
@@ -915,6 +931,8 @@ pub use self::wallet::ExWalletJupiterApeproSmartWalletExecutor;
/// Exposes the reserved `weighted/swap_stabble` executor.
pub use self::weighted::ExWeightedSwapStabbleExecutor;
/// Internal Metaplex Token Metadata prepared-plan builder.
pub(crate) use self::metadata::executor_metaplex_token_metadata_build_prepared_plan;
/// Stable operation code for the nonce advance injected by durable transaction assembly.
pub(crate) use self::solana::EX_SOLANA_TRANSACTION_DURABLE_NONCE_ADVANCE_OPERATION;
/// Maximum over-the-wire size of a Solana transaction packet.
@@ -998,7 +1016,7 @@ pub(crate) use self::spl::executor_spl_token_build_prepared_plan;
mod reserved_executor_tests {
#[test]
fn every_reserved_executor_is_registered_and_builds_only_an_empty_plan() {
let executors: [&dyn crate::ExApiInstructionExecutor; 105] = [
let executors: [&dyn crate::ExApiInstructionExecutor; 104] = [
&crate::ExAdapterSaberDecimalWrapperExecutor,
&crate::ExAdapterSplTokenWrapExecutor,
&crate::ExGovernanceSquadsMultisigExecutor,
@@ -1067,7 +1085,6 @@ mod reserved_executor_tests {
&crate::ExLendingKaminoExecutor,
&crate::ExLendingMarginfiV2Executor,
&crate::ExLockRaydiumLpExecutor,
&crate::ExMetadataMetaplexTokenMetadataExecutor,
&crate::ExMetadataSplNameServiceExecutor,
&crate::ExNftMetaplexBubblegumExecutor,
&crate::ExOrderbookJupiterLimitOrderExecutor,
@@ -1105,7 +1122,7 @@ mod reserved_executor_tests {
&crate::ExWalletJupiterApeproSmartWalletExecutor,
&crate::ExWeightedSwapStabbleExecutor,
];
assert_eq!(executors.len(), 105);
assert_eq!(executors.len(), 104);
for executor in executors {
assert!(!executor.program_ids().is_empty());
for program_id in executor.program_ids() {

View File

@@ -1,14 +1,24 @@
// file: kb-lib/src/executor/metadata.rs
// version: 2
// version: 5
//! `metadata` executor family.
mod metaplex_token_metadata;
mod solana_program_metadata;
mod spl_name_service;
/// Exposes the reserved `metadata/metaplex_token_metadata` executor.
/// Exposes the Metaplex Token Metadata execution surface.
pub use self::metaplex_token_metadata::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION;
pub use self::metaplex_token_metadata::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES;
pub use self::metaplex_token_metadata::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION;
pub use self::metaplex_token_metadata::EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION;
pub use self::metaplex_token_metadata::EX_METAPLEX_TOKEN_METADATA_SUPPORTED_OPERATION_CODES;
pub use self::metaplex_token_metadata::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION;
pub use self::metaplex_token_metadata::ExMetadataMetaplexTokenMetadataExecutor;
pub use self::metaplex_token_metadata::ExMetaplexTokenMetadataExecutionIntent;
pub use self::metaplex_token_metadata::ExMetaplexTokenMetadataOperation;
/// Exposes the reserved `metadata/solana_program_metadata` executor.
pub use self::solana_program_metadata::ExMetadataSolanaProgramMetadataExecutor;
/// Exposes the reserved `metadata/spl_name_service` executor.
pub use self::spl_name_service::ExMetadataSplNameServiceExecutor;
pub(crate) use self::metaplex_token_metadata::executor_metaplex_token_metadata_build_prepared_plan;

View File

@@ -1,49 +1,29 @@
// file: kb-lib/src/executor/metadata/metaplex_token_metadata.rs
// version: 4
// version: 7
//! Reserved executor for `metadata_metaplex_token_metadata`.
//! Metaplex Token Metadata typed execution surface.
/// Reserved executor for the `metadata_metaplex_token_metadata` program surface.
#[derive(Clone, Debug, Default)]
pub struct ExMetadataMetaplexTokenMetadataExecutor;
mod builder;
mod executor;
mod intent;
impl crate::ExApiInstructionExecutor for crate::ExMetadataMetaplexTokenMetadataExecutor {
fn executor_name(&self) -> &'static str {
return "kb-lib.executor.metadata.metaplex_token_metadata";
}
/// Exposes the Metaplex Token Metadata executor.
pub use self::executor::ExMetadataMetaplexTokenMetadataExecutor;
/// Stable operation code for the current Create wrapper.
pub use self::intent::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION;
/// Deprecated operation codes requiring explicit operator approval.
pub use self::intent::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES;
/// Stable deprecated operation code for PuffMetadata.
pub use self::intent::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION;
/// Stable deprecated operation code for SetTokenStandard.
pub use self::intent::EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION;
/// Current and deprecated operations implemented by this prerelease.
pub use self::intent::EX_METAPLEX_TOKEN_METADATA_SUPPORTED_OPERATION_CODES;
/// Stable operation code for the current Update wrapper.
pub use self::intent::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION;
/// Exposes the complete Metaplex Token Metadata execution intent.
pub use self::intent::ExMetaplexTokenMetadataExecutionIntent;
/// Exposes the current and deprecated Metaplex Token Metadata operation intent.
pub use self::intent::ExMetaplexTokenMetadataOperation;
fn executor_version(&self) -> &'static str {
return env!("CARGO_PKG_VERSION");
}
fn program_ids(&self) -> &'static [&'static str] {
return &[kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID];
}
fn supports_request(
&self,
request: &crate::ExApiExecutionRequest,
) -> crate::ExApiExecutionSupport {
if crate::ExApiInstructionExecutor::handles_program_id(self, &request.program_id) {
return crate::ExApiExecutionSupport::Maybe;
}
return crate::ExApiExecutionSupport::No;
}
fn build_plan(
&self,
_request: &crate::ExApiExecutionRequest,
) -> kb_core::Result<crate::ExApiExecutionPlan> {
let payload_json = match crate::executor_api_serialize_payload_json(
&serde_json::json!({"status":"reserved_executor","surface":"metadata.metaplex_token_metadata"}),
) {
std::result::Result::Ok(serialized) => serialized,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::ExApiExecutionPlan {
executor_name: "kb-lib.executor.metadata.metaplex_token_metadata".to_string(),
instruction_count: 0,
payload_json,
});
}
}
pub(crate) use self::builder::executor_metaplex_token_metadata_build_prepared_plan;

View File

@@ -0,0 +1,637 @@
// file: kb-lib/src/executor/metadata/metaplex_token_metadata/builder.rs
// version: 4
//! Official Metaplex builders and conservative plan validation.
use std::str::FromStr; // rust-rules: trait-import
macro_rules! parse_official_pubkey {
($value:expr, $field:expr) => {{
match std::str::FromStr::from_str($value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_pubkey_invalid",
format!("invalid {} public key: {}", $field, error),
));
},
}
}};
}
macro_rules! convert_official_instruction {
($instruction:expr) => {{
let instruction = $instruction;
let program_id = match parse_str_pubkey(&instruction.program_id.to_string(), "program_id") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mut accounts = std::vec::Vec::with_capacity(instruction.accounts.len());
for account in instruction.accounts {
let pubkey = match parse_str_pubkey(&account.pubkey.to_string(), "instruction_account")
{
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
accounts.push(solana_instruction::AccountMeta {
pubkey,
is_signer: account.is_signer,
is_writable: account.is_writable,
});
}
solana_instruction::Instruction {
program_id,
accounts,
data: instruction.data,
}
}};
}
#[allow(deprecated)]
pub(crate) fn executor_metaplex_token_metadata_build_prepared_plan(
intent: &crate::ExMetaplexTokenMetadataExecutionIntent,
) -> kb_core::Result<crate::ExApiPreparedExecutionPlan> {
if let std::result::Result::Err(error) = validate_intent(intent) {
return std::result::Result::Err(error);
}
let instruction_result = match &intent.operation {
crate::ExMetaplexTokenMetadataOperation::Create {
metadata,
master_edition,
mint,
mint_as_signer,
authority,
update_authority,
update_authority_as_signer,
spl_token_program,
create_args,
} => build_create(
intent,
metadata,
master_edition.as_ref(),
mint,
*mint_as_signer,
authority,
update_authority,
*update_authority_as_signer,
spl_token_program.as_ref(),
create_args,
),
crate::ExMetaplexTokenMetadataOperation::UpdateAsUpdateAuthorityV2 {
authority,
mint,
metadata,
edition,
token,
authorization_rules_program,
authorization_rules,
update_args,
} => build_update(
intent,
authority,
mint,
metadata,
edition.as_ref(),
token.as_ref(),
authorization_rules_program.as_ref(),
authorization_rules.as_ref(),
update_args,
),
crate::ExMetaplexTokenMetadataOperation::PuffMetadata { metadata } => {
build_puff_metadata(metadata)
},
crate::ExMetaplexTokenMetadataOperation::SetTokenStandard {
metadata,
update_authority,
mint,
edition,
} => build_set_token_standard(metadata, update_authority, mint, edition.as_ref()),
};
let instruction = match instruction_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let required_signers = required_signers(&intent.fee_payer, &instruction.accounts);
if let std::result::Result::Err(error) = validate_authorized_signers(intent, &required_signers)
{
return std::result::Result::Err(error);
}
return std::result::Result::Ok(crate::ExApiPreparedExecutionPlan {
executor_name: "kb-lib.executor.metadata.metaplex_token_metadata".to_string(),
executor_version: env!("CARGO_PKG_VERSION").to_string(),
intent_id: intent.intent_id.clone(),
operation_code: intent.operation.operation_code().to_string(),
fee_payer: intent.fee_payer.clone(),
instructions: vec![planned_instruction(intent.operation.operation_code(), &instruction)],
required_signers,
policy: intent.policy.clone(),
requested_spend_lamports: intent.max_rent_lamports,
requested_compute_unit_price_micro_lamports: std::option::Option::None,
});
}
fn validate_intent(intent: &crate::ExMetaplexTokenMetadataExecutionIntent) -> kb_core::Result<()> {
if intent.intent_id.trim().is_empty() {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_intent_id_empty",
"Metaplex Token Metadata execution intent id must not be empty",
));
}
if intent.operation.is_deprecated() && !intent.allow_deprecated_operation {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_deprecated_operation_not_approved",
"deprecated Metaplex Token Metadata operation requires explicit operator approval",
));
}
if intent.policy.simulation != crate::ExApiExecutionSimulationPolicy::Required {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_simulation_required",
"Metaplex Token Metadata execution requires simulation before signing or sending",
));
}
if !intent.policy.dry_run {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_dry_run_required",
"Metaplex Token Metadata pre.003 intents are simulation-only",
));
}
match intent.policy.cost_limit.max_fee_lamports {
std::option::Option::Some(value) if value > 0 => {},
std::option::Option::Some(_) | std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_fee_limit_missing",
"Metaplex Token Metadata execution requires a positive transaction fee ceiling",
));
},
}
match intent.policy.cost_limit.max_spend_lamports {
std::option::Option::Some(value) if value >= intent.max_rent_lamports => {},
std::option::Option::Some(_) | std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_spend_limit_insufficient",
"Metaplex Token Metadata execution spend ceiling must cover max_rent_lamports",
));
},
}
return std::result::Result::Ok(());
}
#[allow(clippy::too_many_arguments)]
fn build_create(
intent: &crate::ExMetaplexTokenMetadataExecutionIntent,
metadata: &crate::MdPubkey,
master_edition: std::option::Option<&crate::MdPubkey>,
mint: &crate::MdPubkey,
mint_as_signer: bool,
authority: &crate::MdPubkey,
update_authority: &crate::MdPubkey,
update_authority_as_signer: bool,
spl_token_program: std::option::Option<&crate::MdProgramId>,
create_args: &mpl_token_metadata::types::CreateArgs,
) -> kb_core::Result<solana_instruction::Instruction> {
let metadata = match parse_pubkey(metadata, "metadata") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint = match parse_pubkey(mint, "mint") {
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_metadata_pda(&metadata, &mint) {
return std::result::Result::Err(error);
}
let master_edition = match parse_optional_pubkey(master_edition, "master_edition") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::option::Option::Some(value) = master_edition {
if let std::result::Result::Err(error) = validate_edition_pda(&value, &mint) {
return std::result::Result::Err(error);
}
}
if !matches!(create_args, mpl_token_metadata::types::CreateArgs::V1 { .. }) {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_create_args_unsupported",
"only CreateArgs::V1 is current and supported",
));
}
let official = mpl_token_metadata::instructions::Create {
metadata: parse_official_pubkey!(&metadata.to_string(), "metadata"),
master_edition: match master_edition.as_ref() {
std::option::Option::Some(value) => std::option::Option::Some(parse_official_pubkey!(
&value.to_string(),
"master_edition"
)),
std::option::Option::None => std::option::Option::None,
},
mint: (parse_official_pubkey!(&mint.to_string(), "mint"), mint_as_signer),
authority: parse_official_pubkey!(&authority.0, "authority"),
payer: parse_official_pubkey!(&intent.fee_payer.0, "fee_payer"),
update_authority: (
parse_official_pubkey!(&update_authority.0, "update_authority"),
update_authority_as_signer,
),
system_program: parse_official_pubkey!(kb_program_ids::SYSTEM_PROGRAM_ID, "system_program"),
sysvar_instructions: parse_official_pubkey!(
kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID,
"sysvar_instructions"
),
spl_token_program: match spl_token_program {
std::option::Option::Some(value) => {
std::option::Option::Some(parse_official_pubkey!(&value.0, "spl_token_program"))
},
std::option::Option::None => std::option::Option::None,
},
}
.instruction(mpl_token_metadata::instructions::CreateInstructionArgs {
create_args: create_args.clone(),
});
return std::result::Result::Ok(convert_official_instruction!(official));
}
#[allow(clippy::too_many_arguments)]
fn build_update(
intent: &crate::ExMetaplexTokenMetadataExecutionIntent,
authority: &crate::MdPubkey,
mint: &crate::MdPubkey,
metadata: &crate::MdPubkey,
edition: std::option::Option<&crate::MdPubkey>,
token: std::option::Option<&crate::MdPubkey>,
authorization_rules_program: std::option::Option<&crate::MdProgramId>,
authorization_rules: std::option::Option<&crate::MdPubkey>,
update_args: &mpl_token_metadata::types::UpdateArgs,
) -> kb_core::Result<solana_instruction::Instruction> {
if !matches!(update_args, mpl_token_metadata::types::UpdateArgs::AsUpdateAuthorityV2 { .. }) {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_update_variant_not_yet_enabled",
"pre.003 enables only UpdateArgs::AsUpdateAuthorityV2; delegate variants are scheduled later",
));
}
let mint = match parse_pubkey(mint, "mint") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let metadata = match parse_pubkey(metadata, "metadata") {
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_metadata_pda(&metadata, &mint) {
return std::result::Result::Err(error);
}
let edition = match parse_optional_pubkey(edition, "edition") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::option::Option::Some(value) = edition {
if let std::result::Result::Err(error) = validate_edition_pda(&value, &mint) {
return std::result::Result::Err(error);
}
}
if authorization_rules_program.is_some() != authorization_rules.is_some() {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_rule_set_accounts_incomplete",
"authorization rules program and account must be supplied together",
));
}
let official = mpl_token_metadata::instructions::Update {
authority: parse_official_pubkey!(&authority.0, "authority"),
delegate_record: std::option::Option::None,
token: match token {
std::option::Option::Some(value) => {
std::option::Option::Some(parse_official_pubkey!(&value.0, "token"))
},
std::option::Option::None => std::option::Option::None,
},
mint: parse_official_pubkey!(&mint.to_string(), "mint"),
metadata: parse_official_pubkey!(&metadata.to_string(), "metadata"),
edition: match edition.as_ref() {
std::option::Option::Some(value) => {
std::option::Option::Some(parse_official_pubkey!(&value.to_string(), "edition"))
},
std::option::Option::None => std::option::Option::None,
},
payer: parse_official_pubkey!(&intent.fee_payer.0, "fee_payer"),
system_program: parse_official_pubkey!(kb_program_ids::SYSTEM_PROGRAM_ID, "system_program"),
sysvar_instructions: parse_official_pubkey!(
kb_program_ids::SYSVAR_INSTRUCTIONS_PROGRAM_ID,
"sysvar_instructions"
),
authorization_rules_program: match authorization_rules_program {
std::option::Option::Some(value) => std::option::Option::Some(parse_official_pubkey!(
&value.0,
"authorization_rules_program"
)),
std::option::Option::None => std::option::Option::None,
},
authorization_rules: match authorization_rules {
std::option::Option::Some(value) => {
std::option::Option::Some(parse_official_pubkey!(&value.0, "authorization_rules"))
},
std::option::Option::None => std::option::Option::None,
},
}
.instruction(mpl_token_metadata::instructions::UpdateInstructionArgs {
update_args: update_args.clone(),
});
return std::result::Result::Ok(convert_official_instruction!(official));
}
fn build_puff_metadata(
metadata: &crate::MdPubkey,
) -> kb_core::Result<solana_instruction::Instruction> {
let metadata = match parse_pubkey(metadata, "metadata") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let official = mpl_token_metadata::instructions::PuffMetadata {
metadata: parse_official_pubkey!(&metadata.to_string(), "metadata"),
}
.instruction();
return std::result::Result::Ok(convert_official_instruction!(official));
}
fn build_set_token_standard(
metadata: &crate::MdPubkey,
update_authority: &crate::MdPubkey,
mint: &crate::MdPubkey,
edition: std::option::Option<&crate::MdPubkey>,
) -> kb_core::Result<solana_instruction::Instruction> {
let metadata = match parse_pubkey(metadata, "metadata") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let mint = match parse_pubkey(mint, "mint") {
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_metadata_pda(&metadata, &mint) {
return std::result::Result::Err(error);
}
let edition = match parse_optional_pubkey(edition, "edition") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
if let std::option::Option::Some(value) = edition {
if let std::result::Result::Err(error) = validate_edition_pda(&value, &mint) {
return std::result::Result::Err(error);
}
}
let official = mpl_token_metadata::instructions::SetTokenStandard {
metadata: parse_official_pubkey!(&metadata.to_string(), "metadata"),
update_authority: parse_official_pubkey!(&update_authority.0, "update_authority"),
mint: parse_official_pubkey!(&mint.to_string(), "mint"),
edition: match edition.as_ref() {
std::option::Option::Some(value) => {
std::option::Option::Some(parse_official_pubkey!(&value.to_string(), "edition"))
},
std::option::Option::None => std::option::Option::None,
},
}
.instruction();
return std::result::Result::Ok(convert_official_instruction!(official));
}
fn validate_metadata_pda(
metadata: &solana_pubkey::Pubkey,
mint: &solana_pubkey::Pubkey,
) -> kb_core::Result<()> {
let program_id = match parse_str_pubkey(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
"program_id",
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (expected, _) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program_id.as_ref(), mint.as_ref()],
&program_id,
);
if *metadata != expected {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_metadata_pda_mismatch",
format!("metadata PDA {} does not match expected {}", metadata, expected),
));
}
return std::result::Result::Ok(());
}
fn validate_edition_pda(
edition: &solana_pubkey::Pubkey,
mint: &solana_pubkey::Pubkey,
) -> kb_core::Result<()> {
let program_id = match parse_str_pubkey(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
"program_id",
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let (expected, _) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program_id.as_ref(), mint.as_ref(), b"edition"],
&program_id,
);
if *edition != expected {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_edition_pda_mismatch",
format!("edition PDA {} does not match expected {}", edition, expected),
));
}
return std::result::Result::Ok(());
}
fn validate_authorized_signers(
intent: &crate::ExMetaplexTokenMetadataExecutionIntent,
required: &[crate::ExApiRequiredSigner],
) -> kb_core::Result<()> {
for signer in required {
if !intent.policy.authorized_signers.contains(&signer.pubkey) {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_signer_not_authorized",
format!("required signer {} is not authorized", signer.pubkey.0),
));
}
}
return std::result::Result::Ok(());
}
fn parse_pubkey(value: &crate::MdPubkey, field: &str) -> kb_core::Result<solana_pubkey::Pubkey> {
return parse_str_pubkey(&value.0, field);
}
fn parse_str_pubkey(value: &str, field: &str) -> kb_core::Result<solana_pubkey::Pubkey> {
return solana_pubkey::Pubkey::from_str(value).map_err(|error| {
return kb_core::Error::new(
"execution_metaplex_pubkey_invalid",
format!("invalid {field} public key: {error}"),
);
});
}
fn parse_optional_pubkey(
value: std::option::Option<&crate::MdPubkey>,
field: &str,
) -> kb_core::Result<std::option::Option<solana_pubkey::Pubkey>> {
return match value {
std::option::Option::Some(value) => {
parse_pubkey(value, field).map(std::option::Option::Some)
},
std::option::Option::None => std::result::Result::Ok(std::option::Option::None),
};
}
fn planned_instruction(
operation_code: &str,
instruction: &solana_instruction::Instruction,
) -> crate::ExApiPlannedInstruction {
return crate::ExApiPlannedInstruction {
program_id: crate::MdProgramId(instruction.program_id.to_string()),
operation_code: operation_code.to_string(),
accounts: instruction
.accounts
.iter()
.map(|account| {
return crate::ExApiPlannedAccount {
pubkey: crate::MdPubkey(account.pubkey.to_string()),
is_signer: account.is_signer,
is_writable: account.is_writable,
};
})
.collect(),
data: instruction.data.clone(),
};
}
fn required_signers(
fee_payer: &crate::MdPubkey,
accounts: &[solana_instruction::AccountMeta],
) -> std::vec::Vec<crate::ExApiRequiredSigner> {
let mut required = vec![crate::ExApiRequiredSigner {
pubkey: fee_payer.clone(),
role: "fee_payer".to_string(),
}];
for account in accounts {
if !account.is_signer {
continue;
}
let pubkey = crate::MdPubkey(account.pubkey.to_string());
if required.iter().any(|value| return value.pubkey == pubkey) {
continue;
}
required.push(crate::ExApiRequiredSigner {
pubkey,
role: "metaplex_authority".to_string(),
});
}
return required;
}
#[cfg(test)]
mod tests {
fn approved_puff_intent() -> crate::ExMetaplexTokenMetadataExecutionIntent {
let fee_payer = crate::MdPubkey("11111111111111111111111111111111".to_string());
let mut policy = crate::ExApiExecutionPolicy::default();
policy.cost_limit.max_fee_lamports = std::option::Option::Some(5_000);
policy.cost_limit.max_spend_lamports = std::option::Option::Some(10_000);
policy.authorized_signers = vec![fee_payer.clone()];
return crate::ExMetaplexTokenMetadataExecutionIntent {
intent_id: "metaplex-puff-test".to_string(),
fee_payer,
max_rent_lamports: 10_000,
policy,
allow_deprecated_operation: true,
operation: crate::ExMetaplexTokenMetadataOperation::PuffMetadata {
metadata: crate::MdPubkey(
"SysvarRent111111111111111111111111111111111".to_string(),
),
},
};
}
#[test]
#[allow(deprecated)]
fn deprecated_operation_requires_explicit_approval() {
let mut intent = approved_puff_intent();
intent.allow_deprecated_operation = false;
let result = super::validate_intent(&intent);
assert_eq!(
result.as_ref().err().map(kb_core::Error::code),
std::option::Option::Some("execution_metaplex_deprecated_operation_not_approved"),
);
}
#[test]
#[allow(deprecated)]
fn policy_requires_dry_run_and_sufficient_limits() {
let mut intent = approved_puff_intent();
intent.policy.dry_run = false;
let dry_run_result = super::validate_intent(&intent);
assert_eq!(
dry_run_result.as_ref().err().map(kb_core::Error::code),
std::option::Option::Some("execution_metaplex_dry_run_required"),
);
intent.policy.dry_run = true;
intent.policy.cost_limit.max_spend_lamports = std::option::Option::Some(9_999);
let spend_result = super::validate_intent(&intent);
assert_eq!(
spend_result.as_ref().err().map(kb_core::Error::code),
std::option::Option::Some("execution_metaplex_spend_limit_insufficient"),
);
}
#[test]
fn metadata_and_edition_pda_validation_is_exact() {
let program_id_result = super::parse_str_pubkey(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
"program_id",
);
assert!(program_id_result.is_ok());
let program_id = match program_id_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let mint = solana_pubkey::Pubkey::new_unique();
let (metadata, _) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program_id.as_ref(), mint.as_ref()],
&program_id,
);
let (edition, _) = solana_pubkey::Pubkey::find_program_address(
&[b"metadata", program_id.as_ref(), mint.as_ref(), b"edition"],
&program_id,
);
assert!(super::validate_metadata_pda(&metadata, &mint).is_ok());
assert!(super::validate_edition_pda(&edition, &mint).is_ok());
let wrong = solana_pubkey::Pubkey::new_unique();
let metadata_result = super::validate_metadata_pda(&wrong, &mint);
assert_eq!(
metadata_result.as_ref().err().map(kb_core::Error::code),
std::option::Option::Some("execution_metaplex_metadata_pda_mismatch"),
);
let edition_result = super::validate_edition_pda(&wrong, &mint);
assert_eq!(
edition_result.as_ref().err().map(kb_core::Error::code),
std::option::Option::Some("execution_metaplex_edition_pda_mismatch"),
);
}
#[test]
#[allow(deprecated)]
fn puff_metadata_builds_one_exact_instruction_and_deduplicates_fee_payer() {
let intent = approved_puff_intent();
let result = super::executor_metaplex_token_metadata_build_prepared_plan(&intent);
assert!(result.is_ok());
let plan = match result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(plan.operation_code, crate::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION,);
assert_eq!(plan.instructions.len(), 1);
assert_eq!(
plan.instructions[0].program_id.0,
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID,
);
assert_eq!(plan.instructions[0].data.first().copied(), std::option::Option::Some(14),);
assert_eq!(plan.required_signers.len(), 1);
assert_eq!(plan.required_signers[0].pubkey, intent.fee_payer);
assert_eq!(plan.required_signers[0].role, "fee_payer");
}
}

View File

@@ -0,0 +1,251 @@
// file: kb-lib/src/executor/metadata/metaplex_token_metadata/executor.rs
// version: 3
//! Exact Metaplex Token Metadata capability dispatch.
/// Metaplex Token Metadata executor implementation.
#[derive(Clone, Debug, Default)]
pub struct ExMetadataMetaplexTokenMetadataExecutor;
impl crate::ExMetadataMetaplexTokenMetadataExecutor {
fn exact_capability(
&self,
program_id: &crate::MdProgramId,
operation_code: &str,
) -> crate::ExApiExecutionCapability {
if program_id.0 != kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID {
return crate::ExApiExecutionCapability::unsupported(
"execution_metaplex_program_not_owned",
format!("program {} is not Metaplex Token Metadata", program_id.0),
);
}
if crate::EX_METAPLEX_TOKEN_METADATA_SUPPORTED_OPERATION_CODES.contains(&operation_code) {
return crate::ExApiExecutionCapability::supported(operation_code);
}
return crate::ExApiExecutionCapability::unsupported(
"execution_metaplex_operation_unsupported",
format!("Metaplex Token Metadata operation {operation_code} is not enabled"),
);
}
}
impl crate::ExApiTypedInstructionExecutor for crate::ExMetadataMetaplexTokenMetadataExecutor {
type Intent = crate::ExMetaplexTokenMetadataExecutionIntent;
fn capability(
&self,
program_id: &crate::MdProgramId,
operation_code: &str,
) -> crate::ExApiExecutionCapability {
return self.exact_capability(program_id, operation_code);
}
fn build_prepared_plan(
&self,
intent: &Self::Intent,
) -> kb_core::Result<crate::ExApiPreparedExecutionPlan> {
return crate::executor_metaplex_token_metadata_build_prepared_plan(intent);
}
}
impl crate::ExApiInstructionExecutor for crate::ExMetadataMetaplexTokenMetadataExecutor {
fn executor_name(&self) -> &'static str {
return "kb-lib.executor.metadata.metaplex_token_metadata";
}
fn executor_version(&self) -> &'static str {
return env!("CARGO_PKG_VERSION");
}
fn program_ids(&self) -> &'static [&'static str] {
return &[kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID];
}
fn supports_request(
&self,
request: &crate::ExApiExecutionRequest,
) -> crate::ExApiExecutionSupport {
return match self.exact_capability(&request.program_id, &request.operation_code) {
crate::ExApiExecutionCapability::Supported { operation_code: _ } => {
crate::ExApiExecutionSupport::Yes
},
crate::ExApiExecutionCapability::Unsupported { reason_code: _, reason: _ } => {
crate::ExApiExecutionSupport::No
},
};
}
fn build_plan(
&self,
request: &crate::ExApiExecutionRequest,
) -> kb_core::Result<crate::ExApiExecutionPlan> {
match self.exact_capability(&request.program_id, &request.operation_code) {
crate::ExApiExecutionCapability::Supported { operation_code: _ } => {},
crate::ExApiExecutionCapability::Unsupported { reason_code, reason } => {
return std::result::Result::Err(kb_core::Error::new(reason_code, reason));
},
}
let intent = match serde_json::from_str::<crate::ExMetaplexTokenMetadataExecutionIntent>(
&request.payload_json,
) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_intent_deserialize_failed",
error.to_string(),
));
},
};
if intent.operation.operation_code() != request.operation_code {
return std::result::Result::Err(kb_core::Error::new(
"execution_operation_code_mismatch",
"request operation code does not match the typed Metaplex intent",
));
}
let prepared = match crate::executor_metaplex_token_metadata_build_prepared_plan(&intent) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let payload_value = match serde_json::to_value(&prepared) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::new(
"execution_metaplex_plan_serialize_failed",
error.to_string(),
));
},
};
let payload_json = match crate::executor_api_serialize_payload_json(&payload_value) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return std::result::Result::Ok(crate::ExApiExecutionPlan {
executor_name: self.executor_name().to_string(),
instruction_count: prepared.instructions.len(),
payload_json,
});
}
}
#[cfg(test)]
mod tests {
fn approved_puff_intent() -> crate::ExMetaplexTokenMetadataExecutionIntent {
let fee_payer = crate::MdPubkey("11111111111111111111111111111111".to_string());
let mut policy = crate::ExApiExecutionPolicy::default();
policy.cost_limit.max_fee_lamports = std::option::Option::Some(5_000);
policy.cost_limit.max_spend_lamports = std::option::Option::Some(10_000);
policy.authorized_signers = vec![fee_payer.clone()];
return crate::ExMetaplexTokenMetadataExecutionIntent {
intent_id: "metaplex-executor-test".to_string(),
fee_payer,
max_rent_lamports: 10_000,
policy,
allow_deprecated_operation: true,
operation: crate::ExMetaplexTokenMetadataOperation::PuffMetadata {
metadata: crate::MdPubkey(
"SysvarRent111111111111111111111111111111111".to_string(),
),
},
};
}
#[test]
fn capability_is_exact_for_program_and_operation() {
let executor = crate::ExMetadataMetaplexTokenMetadataExecutor;
let program_id = crate::MdProgramId(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(),
);
let supported = <crate::ExMetadataMetaplexTokenMetadataExecutor as crate::ExApiTypedInstructionExecutor>::capability(
&executor,
&program_id,
crate::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION,
);
assert!(matches!(supported, crate::ExApiExecutionCapability::Supported { .. }));
let unknown = <crate::ExMetadataMetaplexTokenMetadataExecutor as crate::ExApiTypedInstructionExecutor>::capability(
&executor,
&program_id,
"metadata.metaplex_token_metadata.unknown",
);
assert!(matches!(unknown, crate::ExApiExecutionCapability::Unsupported { .. }));
let foreign = <crate::ExMetadataMetaplexTokenMetadataExecutor as crate::ExApiTypedInstructionExecutor>::capability(
&executor,
&crate::MdProgramId(kb_program_ids::SYSTEM_PROGRAM_ID.to_string()),
crate::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION,
);
assert!(matches!(foreign, crate::ExApiExecutionCapability::Unsupported { .. }));
}
#[test]
#[allow(deprecated)]
fn generic_dispatch_rejects_operation_code_mismatch() {
let executor = crate::ExMetadataMetaplexTokenMetadataExecutor;
let intent = approved_puff_intent();
let payload_result = serde_json::to_string(&intent);
assert!(payload_result.is_ok());
let payload_json = match payload_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let request = crate::ExApiExecutionRequest {
program_id: crate::MdProgramId(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(),
),
operation_code: crate::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION.to_string(),
payload_json,
};
let result = <crate::ExMetadataMetaplexTokenMetadataExecutor as crate::ExApiInstructionExecutor>::build_plan(
&executor,
&request,
);
assert_eq!(
result.as_ref().err().map(kb_core::Error::code),
std::option::Option::Some("execution_operation_code_mismatch"),
);
}
#[test]
#[allow(deprecated)]
fn typed_and_generic_dispatch_build_the_same_single_instruction_plan() {
let executor = crate::ExMetadataMetaplexTokenMetadataExecutor;
let intent = approved_puff_intent();
let typed_result = <crate::ExMetadataMetaplexTokenMetadataExecutor as crate::ExApiTypedInstructionExecutor>::build_prepared_plan(
&executor,
&intent,
);
assert!(typed_result.is_ok());
let typed = match typed_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let payload_result = serde_json::to_string(&intent);
assert!(payload_result.is_ok());
let payload_json = match payload_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let request = crate::ExApiExecutionRequest {
program_id: crate::MdProgramId(
kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(),
),
operation_code: crate::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION.to_string(),
payload_json,
};
let generic_result = <crate::ExMetadataMetaplexTokenMetadataExecutor as crate::ExApiInstructionExecutor>::build_plan(
&executor,
&request,
);
assert!(generic_result.is_ok());
let generic = match generic_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(typed.instructions.len(), 1);
assert_eq!(generic.instruction_count, typed.instructions.len());
assert_eq!(generic.executor_name, "kb-lib.executor.metadata.metaplex_token_metadata",);
assert!(
generic
.payload_json
.contains(crate::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION)
);
}
}

View File

@@ -0,0 +1,204 @@
// file: kb-lib/src/executor/metadata/metaplex_token_metadata/intent.rs
// version: 3
//! Typed current and deprecated Metaplex Token Metadata execution intents.
use ts_rs::TS; // rust-rules: derive-import
/// Stable operation code for the current Create wrapper, discriminator 42.
pub const EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION: &str =
"metadata.metaplex_token_metadata.create";
/// Stable operation code for the current Update wrapper, discriminator 50.
pub const EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION: &str =
"metadata.metaplex_token_metadata.update";
/// Stable deprecated operation code for PuffMetadata, discriminator 14.
pub const EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION: &str =
"metadata.metaplex_token_metadata.puff_metadata";
/// Stable deprecated operation code for SetTokenStandard, discriminator 35.
pub const EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION: &str =
"metadata.metaplex_token_metadata.set_token_standard";
/// Deprecated operations that require explicit operator approval at runtime.
pub const EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES: &[&str] = &[
EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION,
EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION,
];
/// Current and deprecated operations implemented by `0.4.7-pre.003`.
pub const EX_METAPLEX_TOKEN_METADATA_SUPPORTED_OPERATION_CODES: &[&str] = &[
EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION,
EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION,
EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION,
EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION,
];
/// One current or explicitly deprecated Metaplex Token Metadata operation.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
#[serde(tag = "operation", rename_all = "snake_case")]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/metaplex_token_metadata/intent/ExMetaplexTokenMetadataOperation.ts"
)]
pub enum ExMetaplexTokenMetadataOperation {
/// Build the current Create wrapper with `CreateArgs::V1`.
Create {
/// Canonical metadata PDA.
metadata: crate::MdPubkey,
/// Optional canonical master edition PDA.
master_edition: std::option::Option<crate::MdPubkey>,
/// Asset mint.
mint: crate::MdPubkey,
/// Whether the mint account signs this create operation.
mint_as_signer: bool,
/// Mint authority signer.
authority: crate::MdPubkey,
/// Update authority.
update_authority: crate::MdPubkey,
/// Whether the update authority signs this create operation.
update_authority_as_signer: bool,
/// Optional SPL Token or Token-2022 program account.
spl_token_program: std::option::Option<crate::MdProgramId>,
/// Official Metaplex create arguments.
#[ts(type = "unknown")]
create_args: mpl_token_metadata::types::CreateArgs,
},
/// Build the current Update wrapper as the update authority.
UpdateAsUpdateAuthorityV2 {
/// Update authority signer.
authority: crate::MdPubkey,
/// Asset mint.
mint: crate::MdPubkey,
/// Canonical metadata PDA.
metadata: crate::MdPubkey,
/// Optional canonical edition PDA.
edition: std::option::Option<crate::MdPubkey>,
/// Optional token account needed by programmable assets.
token: std::option::Option<crate::MdPubkey>,
/// Optional Token Authorization Rules program.
authorization_rules_program: std::option::Option<crate::MdProgramId>,
/// Optional Token Authorization Rules account.
authorization_rules: std::option::Option<crate::MdPubkey>,
/// Official Metaplex update arguments. Only `AsUpdateAuthorityV2` is accepted in this prerelease.
#[ts(type = "unknown")]
update_args: mpl_token_metadata::types::UpdateArgs,
},
/// Build the obsolete PuffMetadata instruction retained for compatibility workflows.
#[deprecated(
since = "0.4.7",
note = "PuffMetadata is obsolete; use only for explicit compatibility workflows"
)]
PuffMetadata {
/// Metadata account to pad to the historical fixed-length representation.
metadata: crate::MdPubkey,
},
/// Build the obsolete SetTokenStandard instruction retained for compatibility workflows.
#[deprecated(
since = "0.4.7",
note = "SetTokenStandard is obsolete; prefer canonical Create or Update flows"
)]
SetTokenStandard {
/// Canonical metadata PDA.
metadata: crate::MdPubkey,
/// Metadata update authority signer.
update_authority: crate::MdPubkey,
/// Asset mint.
mint: crate::MdPubkey,
/// Optional canonical edition PDA.
edition: std::option::Option<crate::MdPubkey>,
},
}
#[allow(deprecated)]
impl crate::ExMetaplexTokenMetadataOperation {
/// Returns the stable generic dispatch operation code.
pub fn operation_code(&self) -> &'static str {
return match self {
Self::Create { .. } => crate::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION,
Self::UpdateAsUpdateAuthorityV2 { .. } => {
crate::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION
},
Self::PuffMetadata { .. } => crate::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION,
Self::SetTokenStandard { .. } => {
crate::EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION
},
};
}
/// Returns whether the operation is obsolete and requires explicit operator approval.
pub fn is_deprecated(&self) -> bool {
return matches!(self, Self::PuffMetadata { .. } | Self::SetTokenStandard { .. });
}
}
/// Complete typed intent accepted by the Metaplex Token Metadata executor.
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
#[ts(
export,
export_to = "../frontend/ts/bindings/kb_lib/executor/metadata/metaplex_token_metadata/intent/ExMetaplexTokenMetadataExecutionIntent.ts"
)]
pub struct ExMetaplexTokenMetadataExecutionIntent {
/// Stable caller-provided identifier used for logs and replay correlation.
pub intent_id: std::string::String,
/// Transaction fee payer.
pub fee_payer: crate::MdPubkey,
/// Maximum rent or account-growth spend allowed for the operation.
pub max_rent_lamports: u64,
/// Conservative common execution policy.
pub policy: crate::ExApiExecutionPolicy,
/// Explicit operator approval required for every deprecated operation.
pub allow_deprecated_operation: bool,
/// Typed Metaplex operation.
pub operation: crate::ExMetaplexTokenMetadataOperation,
}
#[cfg(test)]
mod tests {
#[test]
#[allow(deprecated)]
fn operation_codes_are_exact_unique_and_partition_deprecated_operations() {
assert_eq!(crate::EX_METAPLEX_TOKEN_METADATA_SUPPORTED_OPERATION_CODES.len(), 4);
let mut unique = std::collections::BTreeSet::new();
for operation_code in crate::EX_METAPLEX_TOKEN_METADATA_SUPPORTED_OPERATION_CODES {
assert!(unique.insert(*operation_code));
}
assert_eq!(crate::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES.len(), 2);
assert!(
crate::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES
.contains(&crate::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION,)
);
assert!(
crate::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES
.contains(&crate::EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION,)
);
assert!(
!crate::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES
.contains(&crate::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION,)
);
assert!(
!crate::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES
.contains(&crate::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION,)
);
}
#[test]
#[allow(deprecated)]
fn deprecated_operations_report_their_exact_codes_and_status() {
let puff = crate::ExMetaplexTokenMetadataOperation::PuffMetadata {
metadata: crate::MdPubkey("11111111111111111111111111111111".to_string()),
};
let set_token_standard = crate::ExMetaplexTokenMetadataOperation::SetTokenStandard {
metadata: crate::MdPubkey("11111111111111111111111111111111".to_string()),
update_authority: crate::MdPubkey("11111111111111111111111111111111".to_string()),
mint: crate::MdPubkey("11111111111111111111111111111111".to_string()),
edition: std::option::Option::None,
};
assert_eq!(
puff.operation_code(),
crate::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION,
);
assert_eq!(
set_token_standard.operation_code(),
crate::EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION,
);
assert!(puff.is_deprecated());
assert!(set_token_standard.is_deprecated());
}
}

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/lib.rs
// version: 26
// version: 28
//! Consolidated decoder, executor, materializer and shared model library.
#![warn(missing_docs)]
@@ -358,6 +358,18 @@ pub use self::decoder::decoder_metadata_metaplex_token_metadata_decode_use_autho
pub use self::decoder::decoder_spl_elgamal_registry_parse_elgamal_registry_state;
/// Parses one Token-2022 Mint, Account, or Multisig state.
pub use self::decoder::decoder_spl_token_2022_parse_token_2022_state;
/// Stable operation code for the current Metaplex Create wrapper.
pub use self::executor::EX_METAPLEX_TOKEN_METADATA_CREATE_OPERATION;
/// Deprecated Metaplex operation codes requiring explicit approval.
pub use self::executor::EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES;
/// Stable deprecated Metaplex PuffMetadata operation code.
pub use self::executor::EX_METAPLEX_TOKEN_METADATA_PUFF_METADATA_OPERATION;
/// Stable deprecated Metaplex SetTokenStandard operation code.
pub use self::executor::EX_METAPLEX_TOKEN_METADATA_SET_TOKEN_STANDARD_OPERATION;
/// Current and deprecated Metaplex Token Metadata operations.
pub use self::executor::EX_METAPLEX_TOKEN_METADATA_SUPPORTED_OPERATION_CODES;
/// Stable operation code for the current Metaplex Update wrapper.
pub use self::executor::EX_METAPLEX_TOKEN_METADATA_UPDATE_OPERATION;
/// Exposes the Address Lookup Table close operation code.
pub use self::executor::EX_SOLANA_CORE_ADDRESS_LOOKUP_TABLE_CLOSE_OPERATION;
/// Exposes the Address Lookup Table create operation code.
@@ -1016,12 +1028,16 @@ pub use self::executor::ExLendingKaminoExecutor;
pub use self::executor::ExLendingMarginfiV2Executor;
/// Exposes the reserved `ExLockRaydiumLpExecutor` implementation.
pub use self::executor::ExLockRaydiumLpExecutor;
/// Exposes the reserved `ExMetadataMetaplexTokenMetadataExecutor` implementation.
/// Exposes the Metaplex Token Metadata executor.
pub use self::executor::ExMetadataMetaplexTokenMetadataExecutor;
/// Exposes the reserved `ExMetadataSolanaProgramMetadataExecutor` implementation.
pub use self::executor::ExMetadataSolanaProgramMetadataExecutor;
/// Exposes the reserved `ExMetadataSplNameServiceExecutor` implementation.
pub use self::executor::ExMetadataSplNameServiceExecutor;
/// Exposes the complete Metaplex Token Metadata execution intent.
pub use self::executor::ExMetaplexTokenMetadataExecutionIntent;
/// Exposes one current Metaplex Token Metadata operation.
pub use self::executor::ExMetaplexTokenMetadataOperation;
/// Exposes the reserved `ExNftMetaplexBubblegumExecutor` implementation.
pub use self::executor::ExNftMetaplexBubblegumExecutor;
/// Exposes the reserved `ExNftTensorCnftExecutor` implementation.
@@ -1833,6 +1849,8 @@ pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_MEMO;
pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_TOKEN;
/// Canonical SPL Token-2022 tracing target.
pub(crate) use self::executor::TRACING_TARGET_EXECUTOR_SPL_TOKEN_2022;
/// Internal Metaplex Token Metadata prepared-plan builder.
pub(crate) use self::executor::executor_metaplex_token_metadata_build_prepared_plan;
/// Crate-root access to `build_prepared_plan` from `address_lookup_table`.
pub(crate) use self::executor::executor_solana_core_address_lookup_table_build_prepared_plan;
/// Crate-root access to `build_prepared_plan` from `builder`.