417 lines
20 KiB
Markdown
417 lines
20 KiB
Markdown
<!-- file: kb-lib/USAGE.md -->
|
||
<!-- version: 14 -->
|
||
|
||
# Utilisation de kb-lib
|
||
|
||
## Objectif
|
||
|
||
Ce guide présente les familles d’API publiques significatives de `kb-lib`. Les helpers privés, éléments `pub(crate)` et modules non réexportés ne font pas partie du contrat consommateur.
|
||
|
||
## Dépendance
|
||
|
||
```toml
|
||
[dependencies]
|
||
kb-lib = { path = "../kb-lib" }
|
||
```
|
||
|
||
Toutes les fonctions retournant `kb_core::Result<T>` utilisent le contrat d’erreur structuré du workspace.
|
||
|
||
## Décodage contextualisé
|
||
|
||
`DcApiInstructionDecoder` définit la reconnaissance et le décodage d’une instruction contextualisée. Les décodeurs concrets sont des valeurs sans état.
|
||
|
||
```rust
|
||
use kb_lib::{DcApiInstructionDecoder, DcSolanaCoreDecoder};
|
||
|
||
let decoder = DcSolanaCoreDecoder;
|
||
let recognition = DcApiInstructionDecoder::recognize(&decoder, &input);
|
||
let result = DcApiInstructionDecoder::decode(&decoder, &input);
|
||
```
|
||
|
||
Le consommateur doit vérifier le résultat typé et ses diagnostics. Une transaction échouée peut produire une intention structurée, mais pas une mutation committée.
|
||
|
||
```rust
|
||
let execution = DcApiInstructionDecoder::decode(&decoder, &input);
|
||
|
||
match execution {
|
||
Ok(result) => {
|
||
println!("status={:?}", result.status);
|
||
println!("observations={}", result.observations.len());
|
||
println!("diagnostics={}", result.diagnostics.len());
|
||
},
|
||
Err(error) => return Err(error),
|
||
}
|
||
```
|
||
|
||
Décodeurs fonctionnels principaux :
|
||
|
||
- `DcSolanaCoreDecoder` ;
|
||
- `DcSplMemoDecoder` ;
|
||
- `DcSplTokenDecoder` ;
|
||
- `DcSplAssociatedTokenAccountDecoder` ;
|
||
- `DcSplToken2022Decoder` ;
|
||
- `DcSplElgamalRegistryDecoder` ;
|
||
- `DcMetadataMetaplexTokenMetadataDecoder` ;
|
||
- `DcMetadataSolanaProgramMetadataDecoder`.
|
||
|
||
Les types réservés `Dc*Decoder` ne doivent pas être interprétés comme des décodeurs complets : ils publient une compatibilité conservatrice et aucune observation fonctionnelle.
|
||
|
||
## Lecture d’états Token-2022 et ElGamal
|
||
|
||
```rust
|
||
let state = match kb_lib::decoder_spl_token_2022_parse_token_2022_state(
|
||
kb_lib::DcToken2022StateKind::Account,
|
||
&account_data,
|
||
) {
|
||
Ok(value) => value,
|
||
Err(error) => return Err(error),
|
||
};
|
||
|
||
println!("parsed token-2022 state: {state:#?}");
|
||
```
|
||
|
||
```rust
|
||
let registry = match kb_lib::decoder_spl_elgamal_registry_parse_elgamal_registry_state(
|
||
&account_data,
|
||
) {
|
||
Ok(value) => value,
|
||
Err(error) => return Err(error),
|
||
};
|
||
|
||
println!("parsed registry state: {registry:#?}");
|
||
```
|
||
|
||
Les parseurs vérifient les bornes et formats qu’ils annoncent. Ils ne reconstruisent pas un état historique depuis un RPC courant.
|
||
|
||
## Modèles, comptes et instructions Solana Program Metadata
|
||
|
||
Les modèles `DcMetadataSpm*` représentent uniquement le contrat on-chain du programme `ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S`. Ils ne téléchargent jamais les URL et ne lisent pas automatiquement les comptes référencés par `External`.
|
||
|
||
```rust
|
||
let seed_result = kb_lib::DcMetadataSpmSeed::from_utf8("idl");
|
||
let seed = match seed_result {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||
};
|
||
let external = kb_lib::DcMetadataSpmExternalData::from_zeroable_length(
|
||
solana_pubkey::Pubkey::new_from_array([7_u8; 32]),
|
||
128,
|
||
4096,
|
||
);
|
||
let data = kb_lib::DcMetadataSpmData::External(external);
|
||
let wire_bytes = match data.to_wire_bytes() {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||
};
|
||
assert_eq!(seed.as_bytes().len(), kb_lib::DC_METADATA_SPM_SEED_BYTES);
|
||
assert_eq!(wire_bytes.len(), kb_lib::DC_METADATA_SPM_EXTERNAL_DATA_BYTES);
|
||
```
|
||
|
||
Le champ `length` externe suit le contrat zeroable : la valeur wire `0` devient `None` et signifie que la référence porte sur tout le compte à partir de `offset`. Les contenus `Direct` et `Url` doivent être non vides, et toute longueur doit rester représentable par le `u32` du header.
|
||
|
||
Les trois fonctions publiques de comptes vérifient le propriétaire `ProgM6…`, la borne maximale de données et les layouts fixes :
|
||
|
||
- `decoder_metadata_solana_program_metadata_decode_account` sélectionne `Buffer` ou `Metadata` depuis le discriminateur ;
|
||
- `decoder_metadata_solana_program_metadata_decode_buffer_account` conserve tous les octets alloués après le header, sans prétendre qu’un Buffer garde un PDA vérifiable après une modification d’autorité ;
|
||
- `decoder_metadata_solana_program_metadata_decode_metadata_account` valide le PDA canonical ou non-canonical, `data_length`, les enums fermées et les contraintes propres à `Direct`, `Url` et `External`.
|
||
|
||
```rust
|
||
let snapshot = match kb_lib::decoder_metadata_solana_program_metadata_decode_account(
|
||
metadata_account.as_str(),
|
||
account_owner.as_str(),
|
||
&account_data,
|
||
) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()),
|
||
};
|
||
|
||
match snapshot {
|
||
kb_lib::DcMetadataSpmAccountSnapshot::Buffer(value) => {
|
||
println!("buffer bytes={}", value.allocated_data_bytes);
|
||
},
|
||
kb_lib::DcMetadataSpmAccountSnapshot::Metadata(value) => {
|
||
println!("metadata program={}", value.program);
|
||
println!("logical bytes={}", value.data_length);
|
||
println!("reserved capacity={}", value.trailing_capacity_bytes);
|
||
},
|
||
}
|
||
```
|
||
|
||
Une URL est seulement décodée comme chaîne UTF-8 on-chain. Aucune requête HTTP, IPFS ou Arweave n’est effectuée. Une référence `External` est seulement décodée en adresse, offset et longueur zeroable ; le compte référencé n’est pas lu.
|
||
|
||
Le décodeur `DcMetadataSolanaProgramMetadataDecoder` reconnaît le Program ID `ProgM6…` et les neuf tags stables `0..8`. Il conserve les différences utiles entre le wire SDK et le runtime actuel, sans interpréter un suffixe ignoré comme un nouvel argument.
|
||
|
||
```rust
|
||
let decoder = kb_lib::DcMetadataSolanaProgramMetadataDecoder;
|
||
let recognition = kb_lib::DcApiInstructionDecoder::recognize(&decoder, &input);
|
||
if !recognition.compatible {
|
||
return std::result::Result::Err("instruction incompatible".to_string());
|
||
}
|
||
let result = kb_lib::DcApiInstructionDecoder::decode(&decoder, &input);
|
||
println!("status={:?}", result.status);
|
||
```
|
||
|
||
Le tag `5` est toujours publié comme `trim`. Son ancien nom pré-stable `withdraw_excess_lamports` est conservé uniquement comme provenance, car aucune release stable ne l’expose comme instruction distincte. Les URL et références externes restent des données on-chain ; le décodeur ne réalise aucun fetch.
|
||
|
||
Les trois domaines metadata disposent de matérialiseurs instructionnels spécialisés :
|
||
|
||
- `MtMetadataMetaplexTokenMetadataMaterializer` pour les observations Metaplex de famille metadata ;
|
||
- `MtMetadataSolanaProgramMaterializer` pour les neuf instructions stables de `ProgM6…` ;
|
||
- `MtMetadataToken2022Materializer` pour les instructions incorporées Token Metadata et Token Group de Token-2022.
|
||
|
||
Chaque type possède son propre `componentName`, son propre `processorName`, ses familles acceptées et un ownership exact. Les transactions échouées sont refusées. Les faits d’instructions ne remplacent jamais les snapshots autoritatifs lus après exécution.
|
||
|
||
```rust
|
||
let materializer = kb_lib::MtMetadataSolanaProgramMaterializer;
|
||
if kb_lib::MtApiEventMaterializer::accepts_observation(&materializer, &observation) {
|
||
let result = kb_lib::MtApiEventMaterializer::materialize(&materializer, &observation);
|
||
println!("materialization status={:?}", result.status);
|
||
}
|
||
```
|
||
|
||
Les trois helpers d’état acceptent les snapshots déjà validés par le décodeur :
|
||
|
||
- `materializer_metadata_materialize_solana_program_metadata_account_snapshot` distribue `Buffer` ou `Metadata` ;
|
||
- `materializer_metadata_materialize_solana_program_metadata_buffer_snapshot` conserve tous les octets du Buffer sous une forme bornée avec base64 et SHA-256 ;
|
||
- `materializer_metadata_materialize_solana_program_metadata_metadata_snapshot` conserve le contenu direct, l’URL ou la référence externe, ainsi que l’autorité, la mutabilité, le format et la capacité allouée.
|
||
|
||
```rust
|
||
let output = match kb_lib::materializer_metadata_materialize_solana_program_metadata_account_snapshot(
|
||
slot,
|
||
&snapshot,
|
||
) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
println!("projection={}", output.payload_json);
|
||
```
|
||
|
||
Une URL reste une chaîne observée avec `fetched = false`. Une référence externe reste une adresse, un offset et une longueur avec `accountRead = false`. La matérialisation canonique ne réalise aucun enrichissement off-chain.
|
||
|
||
## Comptes Metaplex Token Metadata
|
||
|
||
Les fonctions `decoder_metadata_metaplex_token_metadata_decode_*_account` décodent les familles de comptes publiques prises en charge. Le consommateur doit fournir les données et le contexte attendus par la signature exacte de la fonction concernée. Les parseurs conservent les variantes historiques et refusent les longueurs, propriétaires ou suffixes incompatibles.
|
||
|
||
```rust
|
||
let metadata = match kb_lib::decoder_metadata_metaplex_token_metadata_decode_metadata_account(
|
||
metadata_account.as_str(),
|
||
metadata_owner.as_str(),
|
||
&account_data,
|
||
) {
|
||
Ok(value) => value,
|
||
Err(error) => return Err(error),
|
||
};
|
||
|
||
println!("metadata mint={}", metadata.mint);
|
||
println!("metadata name={}", metadata.name);
|
||
```
|
||
|
||
```rust
|
||
let edition = match kb_lib::decoder_metadata_metaplex_token_metadata_decode_edition_account(
|
||
edition_account.as_str(),
|
||
metadata_owner.as_str(),
|
||
mint.as_str(),
|
||
&account_data,
|
||
) {
|
||
Ok(value) => value,
|
||
Err(error) => return Err(error),
|
||
};
|
||
|
||
println!("edition kind={:?}", edition.kind);
|
||
println!("edition parent={:?}", edition.parent);
|
||
```
|
||
|
||
Les signatures exactes doivent être vérifiées lors de l’utilisation : certaines familles exigent un contexte ou une identité canonique supplémentaire.
|
||
|
||
## Exécution typée
|
||
|
||
`ExApiTypedInstructionExecutor` produit un `ExApiPreparedExecutionPlan` à partir d’une intention et d’une politique explicites. Les exécuteurs concrets incluent notamment :
|
||
|
||
- `ExSolanaCoreExecutor` ;
|
||
- `ExSplMemoExecutor` pour Memo v4 ;
|
||
- `ExSplTokenExecutor` ;
|
||
- `ExSplAssociatedTokenAccountExecutor` ;
|
||
- `ExSplToken2022Executor` ;
|
||
- `ExSplElgamalRegistryExecutor` ;
|
||
- `ExMetadataSolanaProgramMetadataExecutor`.
|
||
|
||
```rust
|
||
use kb_lib::{ExApiTypedInstructionExecutor, ExSplMemoExecutor};
|
||
|
||
let executor = ExSplMemoExecutor;
|
||
let plan = ExApiTypedInstructionExecutor::build_prepared_plan(
|
||
&executor,
|
||
&intent,
|
||
);
|
||
```
|
||
|
||
Le plan ne signe et n’envoie rien. La simulation, la confirmation opérateur, la soumission et la confirmation réseau appartiennent aux couches d’orchestration et de transport.
|
||
|
||
```rust
|
||
let plan = match ExApiTypedInstructionExecutor::build_prepared_plan(
|
||
&executor,
|
||
&intent,
|
||
) {
|
||
Ok(value) => value,
|
||
Err(error) => return Err(error),
|
||
};
|
||
|
||
println!("instructions={}", plan.instructions.len());
|
||
println!("required_signers={}", plan.required_signers.len());
|
||
```
|
||
|
||
Memo v1 et v3 restent non exécutables. Memo v4 est la seule génération exécutable.
|
||
|
||
|
||
## Exécution Solana Program Metadata
|
||
|
||
`ExMetadataSolanaProgramMetadataExecutor` couvre les neuf opérations stables `Write`, `Initialize`, `SetAuthority`, `SetData`, `SetImmutable`, `Trim`, `Close`, `Allocate` et `Extend`. Les helpers publics dérivent les PDA canonical et non-canonical ; l’appelant doit toujours fournir et vérifier le compte attendu avant construction du plan.
|
||
|
||
Les mutations écrasantes, destructrices ou irréversibles ne sont pas retirées de l’API. Elles exigent `allow_destructive_operation = true`, puis le plan complet est évalué par `ExSafetyChecker`. Cette approbation locale ne remplace ni la simulation RPC, ni la confirmation opérateur, ni les postconditions stateful.
|
||
|
||
```rust
|
||
let intent = kb_lib::ExMetadataSpmExecutionIntent {
|
||
intent_id: "extend-program-metadata".to_string(),
|
||
fee_payer: authority.clone(),
|
||
policy,
|
||
allow_destructive_operation: false,
|
||
operation: kb_lib::ExMetadataSpmOperation::Extend {
|
||
account: metadata_account,
|
||
authority,
|
||
length: 256,
|
||
program_context: std::option::Option::None,
|
||
},
|
||
};
|
||
let executor = kb_lib::ExMetadataSolanaProgramMetadataExecutor;
|
||
let plan = match kb_lib::ExApiTypedInstructionExecutor::build_prepared_plan(
|
||
&executor,
|
||
&intent,
|
||
) {
|
||
std::result::Result::Ok(value) => value,
|
||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||
};
|
||
```
|
||
|
||
`Initialize` et `Allocate` n’inventent pas le financement préalable nécessaire à l’exemption de rent. La préparation de fixture ou l’orchestration stateful doit calculer le financement depuis le RPC, préfinancer le PDA ou le compte keypair, puis simuler le message exact avant soumission.
|
||
|
||
Les contenus inline sont volontairement bornés à 1 024 octets par le workspace. Les contenus plus grands passent par un compte `Buffer` préfinancé, écrit par segments puis consommé par `Initialize` ou `SetData`.
|
||
Pour `Initialize` et les allocations de Buffer PDA, le booléen `canonical` est fourni explicitement. La présence de `program_data` ne suffit pas à déterminer la dérivation, car les programmes issus de loaders antérieurs peuvent être canoniques sans compte ProgramData.
|
||
|
||
## Exécution Metaplex Token Metadata
|
||
|
||
La surface publique repose sur :
|
||
|
||
- `ExMetadataMetaplexTokenMetadataExecutor` ;
|
||
- `ExMetaplexTokenMetadataExecutionIntent` ;
|
||
- `ExMetaplexTokenMetadataOperation` ;
|
||
- les constantes `EX_METAPLEX_TOKEN_METADATA_*_OPERATION` ;
|
||
- `EX_METAPLEX_TOKEN_METADATA_SUPPORTED_OPERATION_CODES` et `EX_METAPLEX_TOKEN_METADATA_DEPRECATED_OPERATION_CODES`.
|
||
|
||
Le consommateur construit un intent typé, puis appelle `ExApiTypedInstructionExecutor::build_prepared_plan`. Le plan retourné contient les instructions, comptes et signers requis, mais n’effectue ni simulation ni envoi.
|
||
|
||
Les opérations courantes utilisent leur wrapper canonique final. Les versions remplacées ne possèdent pas de builder public distinct. Les opérations obsolètes encore constructibles exigent que l’intent autorise explicitement l’exécution dépréciée ; leurs variantes Rust sont marquées `#[deprecated]`.
|
||
|
||
Avant utilisation du plan, le consommateur doit transmettre celui-ci au préflight et à l’orchestration de `kb-pipeline`.
|
||
|
||
### Prérequis des opérations Metaplex
|
||
|
||
Les builders n’inventent aucun compte ni état préalable. L’appelant doit fournir des comptes cohérents avec l’opération :
|
||
|
||
- `Create` NFT classique : mint SPL classique existant, owner `Tokenkeg...`, `decimals = 0`, supply initiale nulle, mint authority et freeze authority correspondant à `authority`, PDA metadata et master edition dérivés du mint, `spl_token_program` explicite et `print_supply` renseigné ;
|
||
- `Update` : metadata existante, update authority courante et comptes programmables éventuels cohérents avec le token standard ;
|
||
- `Verify` / `Unverify` : metadata existante et autorité correspondant exactement au créateur, à la collection ou au delegate visé par `VerificationArgs` ;
|
||
- `Delegate` / `Revoke`, `Lock` / `Unlock`, `Transfer` : token account, token record, delegate record, edition et rule set requis par la variante exacte ;
|
||
- `Burn`, `CloseAccounts` et opérations d’escrow : comptes de destination, token records et autorités de fermeture explicitement fournis ;
|
||
- `Mint`, `Print`, `Migrate`, `Use`, `Collect` et `Resize` : état préalable spécifique déjà créé et compatible avec la variante demandée.
|
||
|
||
Une simulation réseau échouée ne constitue pas une validation de l’opération. Les logs du programme doivent être conservés et la précondition manquante doit être corrigée avant toute soumission.
|
||
|
||
## Sécurité
|
||
|
||
Les types `ExSafetyChecker`, `ExSafetyDecision`, `ExSafetyEvaluation` et `ExSafetyViolation` permettent d’évaluer un plan avant son utilisation. Une décision conservatrice doit être respectée par le consommateur ; elle ne doit pas être contournée par l’application.
|
||
|
||
```rust
|
||
let evaluation = match checker.evaluate_prepared_plan(&plan) {
|
||
Ok(value) => value,
|
||
Err(error) => return Err(error),
|
||
};
|
||
|
||
match evaluation.decision {
|
||
kb_lib::ExSafetyDecision::Allow => {
|
||
println!("execution plan accepted");
|
||
},
|
||
kb_lib::ExSafetyDecision::Deny => {
|
||
for violation in evaluation.violations {
|
||
eprintln!("safety violation: {violation:#?}");
|
||
}
|
||
},
|
||
}
|
||
```
|
||
|
||
## Matérialisation
|
||
|
||
`MtApiEventMaterializer` définit le contrat public de matérialisation. Les résultats `MtApiMaterializerExecutionResult` contiennent les sorties et diagnostics sans dépendre d’un backend de stockage.
|
||
|
||
```rust
|
||
let result = kb_lib::MtApiEventMaterializer::materialize(
|
||
&materializer,
|
||
&decoded_observation,
|
||
);
|
||
|
||
println!("status={:?}", result.status);
|
||
println!("outputs={}", result.outputs.len());
|
||
println!("diagnostics={}", result.diagnostics.len());
|
||
```
|
||
|
||
Les fonctions stateful publiques comprennent notamment :
|
||
|
||
- `materializer_admin_materialize_token_2022_state_snapshots` ;
|
||
- `materializer_admin_materialize_elgamal_registry_state_snapshot` ;
|
||
- `materializer_token_materialize_token_2022_state_snapshot` ;
|
||
- `materializer_metadata_materialize_token_2022_snapshot` ;
|
||
- `materializer_metadata_materialize_metaplex_metadata_snapshot` ;
|
||
- `materializer_metadata_materialize_metaplex_account_snapshot`.
|
||
|
||
## Contrat de replay
|
||
|
||
`MdCoreInstructionReplayInput` représente une instruction extraite avec son contexte transactionnel. Il constitue la frontière entre l’extraction Core, le pipeline de replay et les décodeurs.
|
||
|
||
```rust
|
||
let recognition = DcApiInstructionDecoder::recognize(&decoder, &replay_input);
|
||
|
||
if recognition.compatible {
|
||
let decoded = DcApiInstructionDecoder::decode(&decoder, &replay_input);
|
||
if let Err(error) = decoded {
|
||
return Err(error);
|
||
}
|
||
}
|
||
```
|
||
|
||
## Tests et matrices utiles
|
||
|
||
Les tests unitaires du décodeur Metaplex et du décodeur Solana Core chargent directement certaines matrices de [`../test-fixtures/contract-matrices/`](../test-fixtures/contract-matrices/). Ils vérifient notamment l’égalité entre les entrées compilées et les contrats JSON, les discriminants, les bornes et les comportements historiques.
|
||
|
||
Les tests des exécuteurs comparent les instructions produites aux builders officiels et vérifient signers, comptes ordonnés, coûts et politiques de sécurité.
|
||
|
||
## Limites
|
||
|
||
- aucune persistance ni acquisition réseau ;
|
||
- aucun chargement dynamique des IDL en production ;
|
||
- les squelettes réservés ne sont pas des implémentations ;
|
||
|
||
## Parcours de validation Metaplex Token Metadata sur Devnet
|
||
|
||
La validation de l’exécuteur ne doit pas utiliser un produit cartésien artificiel entre tous les types d’assets et toutes les opérations. Les campagnes doivent sélectionner un parcours cohérent composé d’une fixture, d’un état initial, d’opérations applicables et d’un état terminal attendu.
|
||
|
||
Les familles actuelles sont le NFT classique, le pNFT, le SFT, le fungible et le parcours collection parent/membre. Une collection est une relation et un état supplémentaire, pas un token standard indépendant. Les opérations incompatibles avec une famille doivent être classées `not_applicable`; les opérations valides mais sans fixture doivent être classées `unsupported`.
|
||
|
||
Pour un NFT classique créé par le runner :
|
||
|
||
- le mint SPL classique possède `decimals = 0` et `supply = 0` avant `Create` ;
|
||
- la mint authority et la freeze authority sont toutes deux le wallet du profil ;
|
||
- `spl_token_program` est fourni explicitement ;
|
||
- `print_supply` vaut `Zero` ;
|
||
- un mint dont le PDA metadata est déjà créé ne peut pas être réutilisé pour un second `Create`.
|
||
|
||
La matérialisation Metaplex est optionnelle. Lorsqu’elle est activée après une soumission confirmée, chaque compte déclaré dans les lectures de postcondition est relu, validé, décodé canoniquement puis projeté comme snapshot d’état. Aucun contenu URI off-chain n’est téléchargé par ce parcours.
|