v0.1.0-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: README.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# Khadhroony Bot3
|
||||
|
||||
@@ -53,4 +53,4 @@ cargo clippy --workspace --all-targets
|
||||
|
||||
## Décodeurs consolidés
|
||||
|
||||
`0.1.0-pre.004` porte le décodeur Solana Core dans `kb-lib`. `0.1.0-pre.005` ajoute SPL Memo v1, v3 et v4, avec les mêmes contrats contextualisés, la même façade publique et sans dépendance inverse vers `kb-store`.
|
||||
`0.1.0-pre.004` porte le décodeur Solana Core dans `kb-lib`. `0.1.0-pre.005` ajoute SPL Memo v1, v3 et v4. `0.1.0-pre.006` ajoute le décodeur SPL Token classique avec ses 28 tags publiés, son wire borné, ses formes d’autorité et son `Batch` structuré. Tous utilisent les mêmes contrats contextualisés, la même façade publique et aucune dépendance inverse vers `kb-store`.
|
||||
|
||||
17
ROADMAP.md
17
ROADMAP.md
@@ -1,5 +1,5 @@
|
||||
<!-- file: ROADMAP.md -->
|
||||
<!-- version: 3 -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# ROADMAP — khadhroony-bot3
|
||||
|
||||
@@ -60,5 +60,16 @@
|
||||
- [x] Ajouter les trois Program IDs canoniques dans `kb-program-ids` sans les classer comme natifs.
|
||||
- [x] Préserver le payload borné, le SHA-256, les comptes ordonnés et les statuts non commités.
|
||||
- [x] Porter la matrice machine-readable `docs/SPL_MEMO_MATRIX.json`.
|
||||
- [ ] Valider `kb-program-ids`, `kb-lib` et le workspace avec Cargo sur la machine de développement.
|
||||
- [ ] Poursuivre avec SPL Token classique après validation.
|
||||
- [x] Valider `kb-program-ids`, `kb-lib` et le workspace avec Cargo sur la machine de développement.
|
||||
- [x] Poursuivre avec SPL Token classique après validation.
|
||||
|
||||
### 0.1.0-pre.006 — Décodeur SPL Token classique dans `kb-lib`
|
||||
|
||||
- [x] Remplacer le scaffold `kb-lib::decoder::spl::token` par le décodeur maximal validé de bot2.
|
||||
- [x] Conserver les 28 tags publiés par `spl-token-interface 3.0.0` et les huit tests différentiels.
|
||||
- [x] Préserver montants bruts, comptes ordonnés, autorités simple/multisig, suffixes et statuts non commités.
|
||||
- [x] Conserver les bornes de `Batch`, ses chemins enfants et le refus des batches imbriqués.
|
||||
- [x] Porter le target de tracing vers `kb-lib.decoder.spl.token`.
|
||||
- [x] Rendre la contrainte `wincode 0.5.5` et `solana-wincode-varint 1.0.0` vérifiable par l’audit workspace.
|
||||
- [ ] Valider `kb-program-ids`, `kb-lib`, Clippy et le workspace avec Cargo sur la machine de développement.
|
||||
- [ ] Poursuivre avec le décodeur SPL Associated Token Account après validation.
|
||||
|
||||
7
RULES.md
7
RULES.md
@@ -1,5 +1,5 @@
|
||||
<!-- file: RULES.md -->
|
||||
<!-- version: 17 -->
|
||||
<!-- version: 18 -->
|
||||
|
||||
# Règles spécifiques à `khadhroony-bot3`
|
||||
|
||||
@@ -167,6 +167,11 @@ Aucun renommage massif de modules n'est autorisé sans étape de contrôle dédi
|
||||
- Les features des interfaces doivent rester minimales et explicites. Une feature `serde`, `wincode`, `borsh`, `std`, `alloc` ou équivalente n’est activée que si la crate consommatrice l’utilise réellement.
|
||||
- Les crates applicatives ne doivent pas dépendre d’interfaces RPC ou transactionnelles uniquement pour relayer des types ; ces dépendances appartiennent à `kb_rpc`, aux modèles communs justifiés ou à la crate opérationnelle propriétaire.
|
||||
- Toute exception et toute implémentation locale doivent être documentées dans `docs/SOLANA_INTERFACE_DEPENDENCIES.md` avec la raison, la source officielle et la stratégie de test.
|
||||
- Tant que les types Solana consommés implémentent les traits de `wincode 0.5.x`, le catalogue
|
||||
workspace conserve `wincode = "^0.5"` et le `Cargo.lock` versionné conserve exactement
|
||||
`wincode 0.5.5` avec `solana-wincode-varint 1.0.0`. Une entrée inutilisée ne doit pas être
|
||||
ajoutée artificiellement aux dépendances d’une crate pour influencer le résolveur. L’audit
|
||||
spécifique du workspace refuse toute dérive de ce couple avant compilation.
|
||||
|
||||
## Règles des exécuteurs
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"kb_decoder_spl_associated_token_account",
|
||||
"kb_decoder_spl_elgamal_registry",
|
||||
"kb-lib.decoder.spl.memo",
|
||||
"kb_decoder_spl_token",
|
||||
"kb-lib.decoder.spl.token",
|
||||
"kb_decoder_spl_token_2022",
|
||||
"kb_execution_solana",
|
||||
"kb_executor_metadata_metaplex_token_metadata",
|
||||
@@ -264,42 +264,42 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_kb_decoder_spl_token_debug",
|
||||
"name": "file_kb_lib_decoder_spl_token_debug",
|
||||
"enabled": true,
|
||||
"sink": "file",
|
||||
"level": "debug",
|
||||
"path": "logs/devnet/kb_decoder_spl_token/debug.log",
|
||||
"path": "logs/devnet/kb_lib_decoder_spl_token/debug.log",
|
||||
"rotation": "daily",
|
||||
"format": "human",
|
||||
"ansi": false,
|
||||
"targets": [
|
||||
"kb_decoder_spl_token"
|
||||
"kb-lib.decoder.spl.token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_kb_decoder_spl_token_info",
|
||||
"name": "file_kb_lib_decoder_spl_token_info",
|
||||
"enabled": true,
|
||||
"sink": "file",
|
||||
"level": "info",
|
||||
"path": "logs/devnet/kb_decoder_spl_token/info.log",
|
||||
"path": "logs/devnet/kb_lib_decoder_spl_token/info.log",
|
||||
"rotation": "daily",
|
||||
"format": "human",
|
||||
"ansi": false,
|
||||
"targets": [
|
||||
"kb_decoder_spl_token"
|
||||
"kb-lib.decoder.spl.token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_kb_decoder_spl_token_error",
|
||||
"name": "file_kb_lib_decoder_spl_token_error",
|
||||
"enabled": true,
|
||||
"sink": "file",
|
||||
"level": "error",
|
||||
"path": "logs/devnet/kb_decoder_spl_token/error.jsonl",
|
||||
"path": "logs/devnet/kb_lib_decoder_spl_token/error.jsonl",
|
||||
"rotation": "daily",
|
||||
"format": "json",
|
||||
"ansi": false,
|
||||
"targets": [
|
||||
"kb_decoder_spl_token"
|
||||
"kb-lib.decoder.spl.token"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1629,7 +1629,7 @@
|
||||
"kb_decoder_spl_associated_token_account",
|
||||
"kb_decoder_spl_elgamal_registry",
|
||||
"kb-lib.decoder.spl.memo",
|
||||
"kb_decoder_spl_token",
|
||||
"kb-lib.decoder.spl.token",
|
||||
"kb_decoder_spl_token_2022",
|
||||
"kb_execution_solana",
|
||||
"kb_executor_metadata_metaplex_token_metadata",
|
||||
@@ -1866,42 +1866,42 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_kb_decoder_spl_token_debug",
|
||||
"name": "file_kb_lib_decoder_spl_token_debug",
|
||||
"enabled": true,
|
||||
"sink": "file",
|
||||
"level": "debug",
|
||||
"path": "logs/mainnet_research/kb_decoder_spl_token/debug.log",
|
||||
"path": "logs/mainnet_research/kb_lib_decoder_spl_token/debug.log",
|
||||
"rotation": "daily",
|
||||
"format": "human",
|
||||
"ansi": false,
|
||||
"targets": [
|
||||
"kb_decoder_spl_token"
|
||||
"kb-lib.decoder.spl.token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_kb_decoder_spl_token_info",
|
||||
"name": "file_kb_lib_decoder_spl_token_info",
|
||||
"enabled": true,
|
||||
"sink": "file",
|
||||
"level": "info",
|
||||
"path": "logs/mainnet_research/kb_decoder_spl_token/info.log",
|
||||
"path": "logs/mainnet_research/kb_lib_decoder_spl_token/info.log",
|
||||
"rotation": "daily",
|
||||
"format": "human",
|
||||
"ansi": false,
|
||||
"targets": [
|
||||
"kb_decoder_spl_token"
|
||||
"kb-lib.decoder.spl.token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_kb_decoder_spl_token_error",
|
||||
"name": "file_kb_lib_decoder_spl_token_error",
|
||||
"enabled": true,
|
||||
"sink": "file",
|
||||
"level": "error",
|
||||
"path": "logs/mainnet_research/kb_decoder_spl_token/error.jsonl",
|
||||
"path": "logs/mainnet_research/kb_lib_decoder_spl_token/error.jsonl",
|
||||
"rotation": "daily",
|
||||
"format": "json",
|
||||
"ansi": false,
|
||||
"targets": [
|
||||
"kb_decoder_spl_token"
|
||||
"kb-lib.decoder.spl.token"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -3431,7 +3431,7 @@
|
||||
"kb_decoder_spl_associated_token_account",
|
||||
"kb_decoder_spl_elgamal_registry",
|
||||
"kb-lib.decoder.spl.memo",
|
||||
"kb_decoder_spl_token",
|
||||
"kb-lib.decoder.spl.token",
|
||||
"kb_decoder_spl_token_2022",
|
||||
"kb_execution_solana",
|
||||
"kb_executor_metadata_metaplex_token_metadata",
|
||||
@@ -3668,42 +3668,42 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_kb_decoder_spl_token_debug",
|
||||
"name": "file_kb_lib_decoder_spl_token_debug",
|
||||
"enabled": true,
|
||||
"sink": "file",
|
||||
"level": "debug",
|
||||
"path": "logs/mainnet/kb_decoder_spl_token/debug.log",
|
||||
"path": "logs/mainnet/kb_lib_decoder_spl_token/debug.log",
|
||||
"rotation": "daily",
|
||||
"format": "human",
|
||||
"ansi": false,
|
||||
"targets": [
|
||||
"kb_decoder_spl_token"
|
||||
"kb-lib.decoder.spl.token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_kb_decoder_spl_token_info",
|
||||
"name": "file_kb_lib_decoder_spl_token_info",
|
||||
"enabled": true,
|
||||
"sink": "file",
|
||||
"level": "info",
|
||||
"path": "logs/mainnet/kb_decoder_spl_token/info.log",
|
||||
"path": "logs/mainnet/kb_lib_decoder_spl_token/info.log",
|
||||
"rotation": "daily",
|
||||
"format": "human",
|
||||
"ansi": false,
|
||||
"targets": [
|
||||
"kb_decoder_spl_token"
|
||||
"kb-lib.decoder.spl.token"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "file_kb_decoder_spl_token_error",
|
||||
"name": "file_kb_lib_decoder_spl_token_error",
|
||||
"enabled": true,
|
||||
"sink": "file",
|
||||
"level": "error",
|
||||
"path": "logs/mainnet/kb_decoder_spl_token/error.jsonl",
|
||||
"path": "logs/mainnet/kb_lib_decoder_spl_token/error.jsonl",
|
||||
"rotation": "daily",
|
||||
"format": "json",
|
||||
"ansi": false,
|
||||
"targets": [
|
||||
"kb_decoder_spl_token"
|
||||
"kb-lib.decoder.spl.token"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/SOLANA_INTERFACE_DEPENDENCIES.md -->
|
||||
<!-- version: 28 -->
|
||||
<!-- version: 29 -->
|
||||
|
||||
# Dépendances d’interfaces Solana et SPL
|
||||
|
||||
@@ -29,6 +29,15 @@ Une déclaration à la racine ne justifie pas une dépendance inutilisée dans u
|
||||
|
||||
Le workspace reste épinglé sur `wincode ^0.5`. Les crates Solana modulaires actuellement utilisées avec la feature `wincode` publient leurs implémentations `SchemaRead`/`SchemaWrite` contre `wincode 0.5.x`. Ces traits sont nominalement distincts de ceux de `wincode 0.6.x` : une dépendance directe migrée seule vers `0.6` ne peut donc ni désérialiser `solana_nonce::versions::Versions`, ni sérialiser `solana_transaction::Transaction`. Cargo peut charger les deux versions simultanément, mais les implémentations de traits ne sont pas interchangeables et l’orphan rule interdit de les réimplémenter localement pour ces types externes.
|
||||
|
||||
Le catalogue `[workspace.dependencies]` ne gouverne que les dépendances réellement consommées par
|
||||
les crates membres ; il ne peut pas contraindre à lui seul une branche transitive compatible avec
|
||||
plusieurs versions. Ajouter `solana-wincode-varint` comme dépendance inutilisée de `kb-lib` serait
|
||||
contraire aux règles du workspace. Comme bot3 est un workspace applicatif, son `Cargo.lock`
|
||||
versionné constitue le verrou reproductible de cette branche : il doit résoudre exactement
|
||||
`solana-wincode-varint 1.0.0` et `wincode 0.5.5`. L’audit
|
||||
`scripts/audit_khadhroony_workspace_rules.py` vérifie le manifeste et ces deux versions avant les
|
||||
contrôles Cargo.
|
||||
|
||||
La migration vers `wincode 0.6` doit attendre que l’ensemble des crates Solana consommées migre de façon cohérente. Une double dépendance aliasée n’est acceptable que pour des types propres au workspace ; tous les appels portant sur des types Solana doivent utiliser la même version `0.5.x` que celle employée par leurs crates d’origine. Avant toute migration, vérifier avec `cargo tree -d` et `cargo tree -i wincode@<version>`.
|
||||
|
||||
|
||||
@@ -269,4 +278,3 @@ Le catalogue workspace déclare `mpl-token-metadata ^5.1` avec la seule feature
|
||||
Le dépôt officiel `metaplex-foundation/mpl-token-metadata` confirme le Program ID `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s`. L’IDL officiel audité porte la version `1.14.0` et le blob `5df4a24f62c2743125be096cc174680790c92c18`; l’inventaire Rust généré des instructions porte le blob `3c42eec629f82e44ba690a7c4bd2177cc78f1d49`.
|
||||
|
||||
`kb_decoder_metadata_metaplex_token_metadata` implémente désormais `InstructionDecoder` pour `CreateMetadataAccountV3` et `UpdateMetadataAccountV2`. Les arguments sont lus avec les types Borsh officiels et projetés avec la feature `serde`; les payloads, chaînes, créateurs, frais, comptes et suffixes sont bornés. L’enregistrement dans le registre runtime reste différé jusqu’à validation de ce premier groupe. Les metadata Token-2022 incorporées, Metaplex Core, Bubblegum et le JSON externe restent des provenances ou composants distincts.
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
{
|
||||
"file": "docs/SPL_TOKEN_MATRIX.json",
|
||||
"version": 3,
|
||||
"version": 4,
|
||||
"schemaVersion": 2,
|
||||
"programId": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
|
||||
"bot3Migration": {
|
||||
"milestone": "0.1.0-pre.006",
|
||||
"component": "kb-lib.decoder.spl.token",
|
||||
"status": "decoder_ported_validation_pending",
|
||||
"scope": "decoder_only"
|
||||
},
|
||||
"interface": {
|
||||
"workspaceConstraint": "^3.0",
|
||||
"expectedResolvedVersion": "3.0.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/TRACING_CONTRACT.md -->
|
||||
<!-- version: 14 -->
|
||||
<!-- version: 15 -->
|
||||
|
||||
# Contrat de tracing par crate et composant
|
||||
|
||||
@@ -58,6 +58,7 @@ Les targets déjà migrés vers les noms canoniques bot3 sont :
|
||||
```text
|
||||
kb-lib.decoder.solana.core
|
||||
kb-lib.decoder.spl.memo
|
||||
kb-lib.decoder.spl.token
|
||||
kb-logging
|
||||
kb-store
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# file: kb-lib/Cargo.toml
|
||||
# version: 4
|
||||
# version: 5
|
||||
|
||||
[package]
|
||||
name = "kb-lib"
|
||||
@@ -31,6 +31,7 @@ wincode.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
spl-memo-interface.workspace = true
|
||||
spl-token-interface.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: kb-lib/README.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# kb-lib
|
||||
|
||||
@@ -29,10 +29,25 @@ La matrice `docs/NATIVE_SOLANA_DECODER_MATRIX.json` reste la source machine-read
|
||||
|
||||
Memo v1 ignore les comptes lors de la validation runtime. Memo v3 et v4 exigent que chaque compte fourni soit signer. Les transactions échouées restent des intentions non commitées et les payloads UTF-8 invalides ou signatures manquantes deviennent des tentatives invalides explicites. La matrice normative est `docs/SPL_MEMO_MATRIX.json`.
|
||||
|
||||
## Décodeur SPL Token classique
|
||||
|
||||
`SplTokenDecoder` couvre exclusivement le Program ID classique
|
||||
`TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`. Il publie 28 déclarations de couverture pour
|
||||
les tags `0..24`, `38`, `45` et `255`, y compris les variantes historiques, checked,
|
||||
`WithdrawExcessLamports`, `UnwrapLamports` et `Batch`.
|
||||
|
||||
Le parseur conserve les montants bruts sans perte, les comptes dans leur ordre d’origine, les
|
||||
formes d’autorité simple ou multisig, les suffixes runtime et les chemins outer/inner. Une
|
||||
transaction échouée reste une intention non commitée. `Batch` est borné à 64 enfants, 512 comptes
|
||||
cumulés et interdit les batches imbriqués. Le décodeur n’invente ni mint, ni decimals, ni état
|
||||
final ; les validations `M/N`, soldes et autorités existantes restent stateful. La matrice
|
||||
normative est `docs/SPL_TOKEN_MATRIX.json`.
|
||||
|
||||
## API publique utile
|
||||
|
||||
- `SolanaCoreDecoder` : décodeur concret natif ;
|
||||
- `SplMemoDecoder` : décodeur exact des trois générations SPL Memo ;
|
||||
- `SplTokenDecoder` : décodeur exact du programme SPL Token classique ;
|
||||
- `InstructionDecoder` : contrat de reconnaissance, couverture et décodage contextualisé ;
|
||||
- `ProtocolDecoder` : contrat de compatibilité avec les observations historiques ;
|
||||
- `CoreInstructionReplayInput` : input source-neutral produit par l’extraction core ;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/decoder.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Consolidated decoder modules.
|
||||
|
||||
@@ -49,8 +49,32 @@ pub(crate) use self::spl::SPL_MEMO_V1_SURFACE_CODE;
|
||||
pub(crate) use self::spl::SPL_MEMO_V3_SURFACE_CODE;
|
||||
/// Stable Memo v4 surface code.
|
||||
pub(crate) use self::spl::SPL_MEMO_V4_SURFACE_CODE;
|
||||
/// Maximum prefix retained in bounded classic SPL Token diagnostics.
|
||||
pub(crate) use self::spl::SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable classic SPL Token decoded event contract version.
|
||||
pub(crate) use self::spl::SPL_TOKEN_EVENT_VERSION;
|
||||
/// One exact classic SPL Token instruction entry published by the official interface.
|
||||
pub(crate) use self::spl::SPL_TOKEN_INSTRUCTION_ENTRIES;
|
||||
/// Maximum total account metas consumed by one classic SPL Token batch.
|
||||
pub(crate) use self::spl::SPL_TOKEN_MAX_BATCH_ACCOUNTS;
|
||||
/// Maximum decoded sub-instructions in one classic SPL Token batch.
|
||||
pub(crate) use self::spl::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS;
|
||||
/// Maximum retained classic SPL Token instruction payload size.
|
||||
pub(crate) use self::spl::SPL_TOKEN_MAX_INSTRUCTION_BYTES;
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) use self::spl::SPL_TOKEN_SURFACE_CODE;
|
||||
/// Canonical tracing target for the classic SPL Token decoder.
|
||||
pub(crate) use self::spl::SPL_TOKEN_TRACING_TARGET;
|
||||
/// Decodes one bounded SPL Memo instruction.
|
||||
pub(crate) use self::spl::spl_memo_decode;
|
||||
/// Decodes one bounded classic SPL Token instruction.
|
||||
pub(crate) use self::spl::spl_token_decode;
|
||||
/// Returns the published entry matching one classic SPL Token tag.
|
||||
pub(crate) use self::spl::spl_token_entry_for_tag;
|
||||
/// Returns the first byte of one retained classic SPL Token payload.
|
||||
pub(crate) use self::spl::spl_token_payload_tag;
|
||||
|
||||
/// Exact decoder for the three registered SPL Memo generations.
|
||||
pub use self::spl::SplMemoDecoder;
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
pub use self::spl::SplTokenDecoder;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/decoder/spl.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! `spl` decoder family.
|
||||
|
||||
@@ -10,7 +10,7 @@ pub mod memo;
|
||||
pub mod noop;
|
||||
pub mod single_pool;
|
||||
pub mod stake_pool;
|
||||
pub mod token;
|
||||
mod token;
|
||||
pub mod token_2022;
|
||||
|
||||
/// Maximum payload prefix retained for bounded diagnostics.
|
||||
@@ -31,6 +31,30 @@ pub(crate) use self::memo::SPL_MEMO_V3_SURFACE_CODE;
|
||||
pub(crate) use self::memo::SPL_MEMO_V4_SURFACE_CODE;
|
||||
/// Decodes one bounded SPL Memo instruction.
|
||||
pub(crate) use self::memo::spl_memo_decode;
|
||||
/// Maximum prefix retained in bounded classic SPL Token diagnostics.
|
||||
pub(crate) use self::token::SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable classic SPL Token decoded event contract version.
|
||||
pub(crate) use self::token::SPL_TOKEN_EVENT_VERSION;
|
||||
/// One exact classic SPL Token instruction entry published by the official interface.
|
||||
pub(crate) use self::token::SPL_TOKEN_INSTRUCTION_ENTRIES;
|
||||
/// Maximum total account metas consumed by one classic SPL Token batch.
|
||||
pub(crate) use self::token::SPL_TOKEN_MAX_BATCH_ACCOUNTS;
|
||||
/// Maximum decoded sub-instructions in one classic SPL Token batch.
|
||||
pub(crate) use self::token::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS;
|
||||
/// Maximum retained classic SPL Token instruction payload size.
|
||||
pub(crate) use self::token::SPL_TOKEN_MAX_INSTRUCTION_BYTES;
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) use self::token::SPL_TOKEN_SURFACE_CODE;
|
||||
/// Canonical tracing target for the classic SPL Token decoder.
|
||||
pub(crate) use self::token::SPL_TOKEN_TRACING_TARGET;
|
||||
/// Decodes one bounded classic SPL Token instruction.
|
||||
pub(crate) use self::token::spl_token_decode;
|
||||
/// Returns the published entry matching one classic SPL Token tag.
|
||||
pub(crate) use self::token::spl_token_entry_for_tag;
|
||||
/// Returns the first byte of one retained classic SPL Token payload.
|
||||
pub(crate) use self::token::spl_token_payload_tag;
|
||||
|
||||
/// Exact decoder for the three registered SPL Memo generations.
|
||||
pub use self::memo::SplMemoDecoder;
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
pub use self::token::SplTokenDecoder;
|
||||
|
||||
@@ -1,10 +1,34 @@
|
||||
// file: kb-lib/src/decoder/spl/token.rs
|
||||
// version: 1
|
||||
// version: 2
|
||||
|
||||
//! Migration boundary for legacy crate `kb_decoder_spl_token`.
|
||||
//! Exact classic SPL Token decoder component.
|
||||
|
||||
/// Legacy crate name retained for migration and compatibility tracking.
|
||||
pub const LEGACY_CRATE: &str = "kb_decoder_spl_token";
|
||||
mod constants;
|
||||
mod decoder;
|
||||
mod wire;
|
||||
|
||||
/// Current porting status.
|
||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
||||
/// Maximum prefix retained in bounded diagnostics.
|
||||
pub(crate) use self::constants::SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable decoded event contract version.
|
||||
pub(crate) use self::constants::SPL_TOKEN_EVENT_VERSION;
|
||||
/// One exact instruction entry published by `spl-token-interface` 3.0.0.
|
||||
pub(crate) use self::constants::SPL_TOKEN_INSTRUCTION_ENTRIES;
|
||||
/// Maximum total account metas consumed by one batch.
|
||||
pub(crate) use self::constants::SPL_TOKEN_MAX_BATCH_ACCOUNTS;
|
||||
/// Maximum decoded sub-instructions in one batch.
|
||||
pub(crate) use self::constants::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS;
|
||||
/// Maximum retained instruction payload size.
|
||||
pub(crate) use self::constants::SPL_TOKEN_MAX_INSTRUCTION_BYTES;
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) use self::constants::SPL_TOKEN_SURFACE_CODE;
|
||||
/// Canonical tracing target for the classic SPL Token decoder.
|
||||
pub(crate) use self::constants::SPL_TOKEN_TRACING_TARGET;
|
||||
/// Decodes one bounded classic SPL Token instruction.
|
||||
pub(crate) use self::wire::decode as spl_token_decode;
|
||||
/// Returns the published entry matching one classic SPL Token tag.
|
||||
pub(crate) use self::wire::entry_for_tag as spl_token_entry_for_tag;
|
||||
/// Returns the first byte of one retained classic SPL Token payload.
|
||||
pub(crate) use self::wire::payload_tag as spl_token_payload_tag;
|
||||
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
pub use self::decoder::SplTokenDecoder;
|
||||
|
||||
50
kb-lib/src/decoder/spl/token/constants.rs
Normal file
50
kb-lib/src/decoder/spl/token/constants.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
// file: kb-lib/src/decoder/spl/token/constants.rs
|
||||
// version: 1
|
||||
|
||||
//! Local constants for the `kb-lib` SPL Token component. Program identifiers live in `kb_program_ids`.
|
||||
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) const SPL_TOKEN_SURFACE_CODE: &str = "spl_token";
|
||||
/// Current stable decoded event contract version.
|
||||
pub(crate) const SPL_TOKEN_EVENT_VERSION: u32 = 1;
|
||||
/// Maximum retained instruction payload size.
|
||||
pub(crate) const SPL_TOKEN_MAX_INSTRUCTION_BYTES: usize = 16_384;
|
||||
/// Maximum prefix retained in diagnostics.
|
||||
pub(crate) const SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES: usize = 32;
|
||||
/// Maximum decoded sub-instructions in one batch.
|
||||
pub(crate) const SPL_TOKEN_MAX_BATCH_INSTRUCTIONS: usize = 64;
|
||||
/// Maximum total account metas consumed by one batch.
|
||||
pub(crate) const SPL_TOKEN_MAX_BATCH_ACCOUNTS: usize = 512;
|
||||
/// One exact instruction entry published by `spl-token-interface` 3.0.0.
|
||||
pub(crate) const SPL_TOKEN_INSTRUCTION_ENTRIES: &[(u8, &str, bool)] = &[
|
||||
(0, "initialize_mint", true),
|
||||
(1, "initialize_account", true),
|
||||
(2, "initialize_multisig", true),
|
||||
(3, "transfer", true),
|
||||
(4, "approve", true),
|
||||
(5, "revoke", true),
|
||||
(6, "set_authority", true),
|
||||
(7, "mint_to", true),
|
||||
(8, "burn", true),
|
||||
(9, "close_account", true),
|
||||
(10, "freeze_account", true),
|
||||
(11, "thaw_account", true),
|
||||
(12, "transfer_checked", false),
|
||||
(13, "approve_checked", false),
|
||||
(14, "mint_to_checked", false),
|
||||
(15, "burn_checked", false),
|
||||
(16, "initialize_account2", true),
|
||||
(17, "sync_native", false),
|
||||
(18, "initialize_account3", false),
|
||||
(19, "initialize_multisig2", false),
|
||||
(20, "initialize_mint2", false),
|
||||
(21, "get_account_data_size", false),
|
||||
(22, "initialize_immutable_owner", false),
|
||||
(23, "amount_to_ui_amount", false),
|
||||
(24, "ui_amount_to_amount", false),
|
||||
(38, "withdraw_excess_lamports", false),
|
||||
(45, "unwrap_lamports", false),
|
||||
(255, "batch", false),
|
||||
];
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const SPL_TOKEN_TRACING_TARGET: &str = "kb-lib.decoder.spl.token";
|
||||
500
kb-lib/src/decoder/spl/token/decoder.rs
Normal file
500
kb-lib/src/decoder/spl/token/decoder.rs
Normal file
@@ -0,0 +1,500 @@
|
||||
// file: kb-lib/src/decoder/spl/token/decoder.rs
|
||||
// version: 1
|
||||
|
||||
//! Exact classic SPL Token dispatch for the common decode pipeline.
|
||||
|
||||
const TOKEN_SURFACES: &[crate::DecoderSurface] = &[crate::DecoderSurface {
|
||||
program_id: kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
surface_code: crate::SPL_TOKEN_SURFACE_CODE,
|
||||
priority: 100,
|
||||
}];
|
||||
|
||||
const PROGRAM_IDS: &[&str] = &[kb_program_ids::SPL_TOKEN_PROGRAM_ID];
|
||||
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SplTokenDecoder;
|
||||
|
||||
impl crate::ProtocolDecoder for crate::SplTokenDecoder {
|
||||
fn decoder_name(&self) -> &'static str {
|
||||
return "kb_decoder_spl_token";
|
||||
}
|
||||
|
||||
fn decoder_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn program_ids(&self) -> &'static [&'static str] {
|
||||
return PROGRAM_IDS;
|
||||
}
|
||||
|
||||
fn supports_observation(
|
||||
&self,
|
||||
observation: &crate::ProgramObservation,
|
||||
) -> crate::DecoderSupport {
|
||||
return if crate::ProtocolDecoder::handles_program_id(self, &observation.program_id) {
|
||||
crate::DecoderSupport::Yes
|
||||
} else {
|
||||
crate::DecoderSupport::No
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_observation(
|
||||
&self,
|
||||
_observation: &crate::ProgramObservation,
|
||||
) -> kb_core::Result<std::vec::Vec<crate::DecodedProtocolEvent>> {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::InstructionDecoder for crate::SplTokenDecoder {
|
||||
fn identity(&self) -> crate::DecoderIdentity {
|
||||
return crate::DecoderIdentity {
|
||||
name: "spl_token".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn surfaces(&self) -> &'static [crate::DecoderSurface] {
|
||||
return TOKEN_SURFACES;
|
||||
}
|
||||
|
||||
fn coverage(&self) -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
|
||||
return crate::SPL_TOKEN_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.map(|(tag, code, historical)| {
|
||||
return crate::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(
|
||||
crate::SPL_TOKEN_SURFACE_CODE.to_string(),
|
||||
),
|
||||
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: (*code).to_string(),
|
||||
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
|
||||
historical: *historical,
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn recognize(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderRecognition {
|
||||
if input.program_id != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
||||
return crate::DecoderRecognition::incompatible();
|
||||
}
|
||||
let tag = crate::spl_token_payload_tag(input);
|
||||
let entry = tag.and_then(crate::spl_token_entry_for_tag);
|
||||
return crate::DecoderRecognition::compatible(
|
||||
entry.is_some(),
|
||||
100,
|
||||
std::option::Option::Some(crate::SPL_TOKEN_SURFACE_CODE.to_string()),
|
||||
entry.map(|(_, code, _)| return code.to_string()),
|
||||
tag.map(|value| return format!("{value:02x}")),
|
||||
);
|
||||
}
|
||||
|
||||
fn decode(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
|
||||
if input.program_id != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
||||
return crate::DecoderExecutionResult::unsupported(std::option::Option::None);
|
||||
}
|
||||
let result = crate::spl_token_decode(input);
|
||||
if matches!(
|
||||
result.status,
|
||||
crate::DecoderOutcomeStatus::Failed | crate::DecoderOutcomeStatus::Unsupported
|
||||
) {
|
||||
tracing::error!(
|
||||
target: crate::SPL_TOKEN_TRACING_TARGET,
|
||||
action = "decode_failure",
|
||||
signature = %input.signature,
|
||||
slot = input.slot,
|
||||
instruction_path = %input.instruction_path,
|
||||
program_id = %input.program_id,
|
||||
processor_name = "spl_token",
|
||||
processor_version = env!("CARGO_PKG_VERSION"),
|
||||
input_key = %input.replay_input_key,
|
||||
payload_hash = ?input.instruction_payload_hash,
|
||||
result_status = ?result.status,
|
||||
diagnostics = ?result.diagnostics,
|
||||
"classic SPL Token instruction was not decoded successfully"
|
||||
);
|
||||
}
|
||||
tracing::debug!(
|
||||
target: crate::SPL_TOKEN_TRACING_TARGET,
|
||||
action = "decode",
|
||||
signature = %input.signature,
|
||||
instruction_path = %input.instruction_path,
|
||||
transaction_failed = input.transaction_failed,
|
||||
result_status = ?result.status,
|
||||
observation_count = result.observations.len(),
|
||||
"classic SPL Token instruction decode completed"
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn input(
|
||||
program_id: &str,
|
||||
payload: &[u8],
|
||||
accounts: serde_json::Value,
|
||||
keys: serde_json::Value,
|
||||
failed: bool,
|
||||
path: &str,
|
||||
) -> crate::CoreInstructionReplayInput {
|
||||
let result = crate::CoreInstructionReplayInput::new(
|
||||
format!("signature:{path}"),
|
||||
"signature",
|
||||
42,
|
||||
path,
|
||||
program_id,
|
||||
failed,
|
||||
if failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
keys,
|
||||
accounts,
|
||||
std::option::Option::Some(serde_json::json!({
|
||||
"dataBase64": base64::engine::general_purpose::STANDARD.encode(payload)
|
||||
})),
|
||||
std::option::Option::None,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("Token replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn minimal_wire(tag: u8) -> std::vec::Vec<u8> {
|
||||
return match tag {
|
||||
0 | 20 => {
|
||||
let mut value = std::vec![tag, 9];
|
||||
value.extend_from_slice(&[1; 32]);
|
||||
value.push(0);
|
||||
value
|
||||
},
|
||||
2 | 19 => std::vec![tag, 1],
|
||||
3 | 4 | 7 | 8 | 23 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&1_u64.to_le_bytes());
|
||||
value
|
||||
},
|
||||
6 => std::vec![tag, 0, 0],
|
||||
12 | 13 | 14 | 15 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&1_u64.to_le_bytes());
|
||||
value.push(9);
|
||||
value
|
||||
},
|
||||
16 | 18 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&[2; 32]);
|
||||
value
|
||||
},
|
||||
24 => std::vec![tag, b'1'],
|
||||
45 => std::vec![tag, 0],
|
||||
255 => std::vec![tag, 0, 1, 1],
|
||||
_ => std::vec![tag],
|
||||
};
|
||||
}
|
||||
|
||||
fn account(position: usize, signer: bool, writable: bool) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"position": position,
|
||||
"accountIndex": position,
|
||||
"accountKey": format!("account{position}"),
|
||||
"signer": signer,
|
||||
"writable": writable,
|
||||
"source": "static",
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_support_program_id_and_interface_match() {
|
||||
assert_eq!(spl_token_interface::ID.to_string(), kb_program_ids::SPL_TOKEN_PROGRAM_ID);
|
||||
let decoder = crate::SplTokenDecoder;
|
||||
assert_eq!(crate::InstructionDecoder::surfaces(&decoder).len(), 1);
|
||||
let observation = crate::ProgramObservation {
|
||||
signature: crate::Signature("signature".to_string()),
|
||||
slot: crate::Slot(1),
|
||||
instruction_path: crate::InstructionPath("0".to_string()),
|
||||
program_id: crate::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
|
||||
discriminator_8: std::option::Option::None,
|
||||
data_len: 1,
|
||||
accounts_len: 0,
|
||||
failed: false,
|
||||
};
|
||||
assert_eq!(
|
||||
crate::ProtocolDecoder::supports_observation(&decoder, &observation),
|
||||
crate::DecoderSupport::Yes
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_matrix_and_compiled_entries_are_equal() {
|
||||
let matrix: serde_json::Value =
|
||||
match serde_json::from_str(include_str!("../../../../../docs/SPL_TOKEN_MATRIX.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("SPL Token matrix is invalid: {error}"),
|
||||
};
|
||||
let rows = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("SPL Token matrix has no instructions"),
|
||||
};
|
||||
let matrix_entries = rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
return (
|
||||
row.get("tag").and_then(serde_json::Value::as_u64).unwrap_or(u64::MAX) as u8,
|
||||
row.get("name").and_then(serde_json::Value::as_str).unwrap_or(""),
|
||||
);
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let compiled_entries = crate::SPL_TOKEN_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.map(|(tag, code, _)| return (*tag, *code))
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(matrix_entries, compiled_entries);
|
||||
assert_eq!(crate::InstructionDecoder::coverage(&crate::SplTokenDecoder).len(), 28);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_evidence_scopes_recent_instruction_deployment_exactly() {
|
||||
let matrix: serde_json::Value =
|
||||
match serde_json::from_str(include_str!("../../../../../docs/SPL_TOKEN_MATRIX.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("SPL Token matrix is invalid: {error}"),
|
||||
};
|
||||
assert_eq!(
|
||||
matrix["deploymentAudit"]["mainnet"]["status"],
|
||||
"real_corpus_decode_and_materialization_validated"
|
||||
);
|
||||
assert_eq!(matrix["corpusAudit"]["mainnet"]["invariants"]["uncommittedOutputs"], 0);
|
||||
assert_eq!(matrix["corpusAudit"]["mainnet"]["invariants"]["secondReplaySelected"], 0);
|
||||
assert_eq!(matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["submission"], false);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["batch"]["success"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["batch"]["computeUnits"],
|
||||
270
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["unwrapLamports"]["success"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["unwrapLamports"]["computeUnits"],
|
||||
140
|
||||
);
|
||||
|
||||
let instructions = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("SPL Token matrix has no instructions"),
|
||||
};
|
||||
for recent_name in ["unwrap_lamports", "batch"] {
|
||||
let recent = match instructions.iter().find(|row| {
|
||||
return row.get("name").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(recent_name);
|
||||
}) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
panic!("recent SPL Token instruction {recent_name} is absent")
|
||||
},
|
||||
};
|
||||
assert_eq!(recent["executorSupport"]["constructibility"], "official_builder_proven");
|
||||
assert_eq!(
|
||||
recent["executorSupport"]["clusterDeployment"],
|
||||
"devnet_simulation_succeeded_2026-07-16"
|
||||
);
|
||||
assert_eq!(recent["clusterStatus"]["localnet"], "not_tested");
|
||||
assert_eq!(recent["clusterStatus"]["devnet"], "simulation_succeeded_2026-07-16");
|
||||
assert_eq!(recent["clusterStatus"]["mainnet"], "not_tested");
|
||||
assert_eq!(recent["realSignatures"].as_array().map(std::vec::Vec::len), Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_published_tag_decodes_and_matches_official_unpack() {
|
||||
for (tag, code, _) in crate::SPL_TOKEN_INSTRUCTION_ENTRIES {
|
||||
let wire = minimal_wire(*tag);
|
||||
let official = spl_token_interface::instruction::TokenInstruction::unpack(&wire);
|
||||
assert!(official.is_ok(), "official unpack rejected tag {tag}");
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some(*code));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transfer_is_an_exact_uncommitted_inner_intent_without_invented_mint() {
|
||||
let mut wire = std::vec![3];
|
||||
wire.extend_from_slice(&u64::MAX.to_le_bytes());
|
||||
let accounts = serde_json::json!([
|
||||
{"position":0,"accountIndex":0,"accountKey":"source"},
|
||||
{"position":1,"accountIndex":1,"accountKey":"destination"},
|
||||
{"position":2,"accountIndex":2,"accountKey":"authority"}
|
||||
]);
|
||||
let keys = serde_json::json!([
|
||||
{"accountIndex":0,"accountKey":"source","signer":false,"writable":true,"source":"static"},
|
||||
{"accountIndex":1,"accountKey":"destination","signer":false,"writable":true,"source":"static"},
|
||||
{"accountIndex":2,"accountKey":"authority","signer":true,"writable":false,"source":"static"}
|
||||
]);
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
accounts,
|
||||
keys,
|
||||
true,
|
||||
"2/1",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["amountRaw"],
|
||||
u64::MAX.to_string()
|
||||
);
|
||||
assert_eq!(result.observations[0].payload_json["inference"]["mintInvented"], false);
|
||||
assert_eq!(
|
||||
result.observations[0].event.source_kind,
|
||||
crate::EventSourceKind::InnerInstruction
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suffix_unknown_empty_truncated_and_invalid_utf8_are_bounded() {
|
||||
let mut transfer = minimal_wire(3);
|
||||
transfer.extend_from_slice(&[0xaa, 0xbb]);
|
||||
let suffix = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
transfer.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let suffix_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &suffix);
|
||||
assert_eq!(suffix_result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(suffix_result.observations[0].payload_json["wire"]["suffixLengthBytes"], 2);
|
||||
for wire in [std::vec::Vec::new(), std::vec![3, 1], std::vec![24, 0xff], std::vec![255]] {
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed);
|
||||
assert!(result.observations.is_empty());
|
||||
}
|
||||
let unknown = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
&[44],
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &unknown);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Unsupported);
|
||||
assert!(!result.diagnostics.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_preserves_order_slices_paths_and_rejects_nested_batch() {
|
||||
let mut batch = std::vec![255, 0, 9, 3];
|
||||
batch.extend_from_slice(&1_u64.to_le_bytes());
|
||||
batch.extend_from_slice(&[0, 1, 17]);
|
||||
let cinput = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
batch.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"4/2",
|
||||
);
|
||||
let result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &cinput);
|
||||
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.observations.len(), 3);
|
||||
assert_eq!(result.observations[1].event.instruction_path.0, "4/2/batch/0");
|
||||
assert_eq!(result.observations[2].event.instruction_path.0, "4/2/batch/1");
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["subInstructionCount"], 2);
|
||||
let nested = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
&[255, 0, 1, 255],
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let nested_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &nested);
|
||||
assert_eq!(nested_result.status, crate::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authority_forms_and_multisig_threshold_diagnostics_are_explicit() {
|
||||
let wire = minimal_wire(3);
|
||||
let simple_accounts = serde_json::json!([
|
||||
{"position":0,"accountIndex":0,"accountKey":"account0"},
|
||||
{"position":1,"accountIndex":1,"accountKey":"account1"},
|
||||
{"position":2,"accountIndex":2,"accountKey":"account2"}
|
||||
]);
|
||||
let simple_keys = serde_json::json!([
|
||||
account(0, false, true),
|
||||
account(1, false, true),
|
||||
account(2, true, false)
|
||||
]);
|
||||
let simple = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
simple_accounts.clone(),
|
||||
simple_keys,
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let simple_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &simple);
|
||||
assert_eq!(simple_result.observations[0].payload_json["authority"]["form"], "single");
|
||||
let mut multisig_accounts = simple_accounts.as_array().cloned().unwrap_or_default();
|
||||
multisig_accounts
|
||||
.push(serde_json::json!({"position":3,"accountIndex":3,"accountKey":"account3"}));
|
||||
let multisig_keys = serde_json::json!([
|
||||
account(0, false, true),
|
||||
account(1, false, true),
|
||||
account(2, false, false),
|
||||
account(3, true, false)
|
||||
]);
|
||||
let multisig = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::Value::Array(multisig_accounts),
|
||||
multisig_keys,
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let multisig_result = crate::InstructionDecoder::decode(&crate::SplTokenDecoder, &multisig);
|
||||
assert_eq!(multisig_result.observations[0].payload_json["authority"]["form"], "multisig");
|
||||
assert_eq!(
|
||||
multisig_result.observations[0].payload_json["authority"]["statefulMultisigValidation"],
|
||||
"requires_multisig_account_snapshot"
|
||||
);
|
||||
}
|
||||
}
|
||||
999
kb-lib/src/decoder/spl/token/wire.rs
Normal file
999
kb-lib/src/decoder/spl/token/wire.rs
Normal file
@@ -0,0 +1,999 @@
|
||||
// file: kb-lib/src/decoder/spl/token/wire.rs
|
||||
// version: 1
|
||||
|
||||
//! Bounded classic SPL Token wire, account and authority decoding.
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ParsedInstruction {
|
||||
code: &'static str,
|
||||
tag: u8,
|
||||
parameters: serde_json::Value,
|
||||
consumed: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ResolvedAccount {
|
||||
position: usize,
|
||||
account_index: u64,
|
||||
account_key: std::string::String,
|
||||
signer: bool,
|
||||
writable: bool,
|
||||
source: serde_json::Value,
|
||||
}
|
||||
|
||||
pub(crate) fn entry_for_tag(tag: u8) -> std::option::Option<(u8, &'static str, bool)> {
|
||||
return crate::SPL_TOKEN_INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|(candidate, _, _)| return *candidate == tag);
|
||||
}
|
||||
|
||||
pub(crate) fn payload_tag(input: &crate::CoreInstructionReplayInput) -> std::option::Option<u8> {
|
||||
let bytes = decode_payload(input).ok();
|
||||
return bytes.and_then(|value| return value.first().copied());
|
||||
}
|
||||
|
||||
pub(crate) fn decode(input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
|
||||
let bytes = match decode_payload(input) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return failed(
|
||||
std::option::Option::None,
|
||||
"token_payload_unavailable",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let tag = match bytes.first().copied() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return failed(
|
||||
std::option::Option::None,
|
||||
"token_payload_empty",
|
||||
"classic SPL Token instruction payload is empty".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let entry = match crate::spl_token_entry_for_tag(tag) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Unsupported,
|
||||
recognized_entry_code: std::option::Option::None,
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: std::vec![crate::DecoderDiagnostic {
|
||||
code: "unknown_token_instruction_tag".to_string(),
|
||||
message: format!(
|
||||
"unknown classic SPL Token tag {tag}; payloadPrefixHex={}",
|
||||
hex_prefix(bytes.as_slice())
|
||||
),
|
||||
retriable: false,
|
||||
}],
|
||||
};
|
||||
},
|
||||
};
|
||||
let parsed = match parse_instruction(bytes.as_slice(), false) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(message) => {
|
||||
return failed(
|
||||
std::option::Option::Some(entry.1),
|
||||
"malformed_token_instruction",
|
||||
format!("{message}; payloadPrefixHex={}", hex_prefix(bytes.as_slice())),
|
||||
);
|
||||
},
|
||||
};
|
||||
let accounts = match resolve_accounts(input) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return failed(
|
||||
std::option::Option::Some(parsed.code),
|
||||
"token_accounts_malformed",
|
||||
error.to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let (parameters, batch_children) = if parsed.code == "batch" {
|
||||
match batch_observations(input, bytes.as_slice(), accounts.as_slice()) {
|
||||
std::result::Result::Ok((children, manifest)) => (manifest, children),
|
||||
std::result::Result::Err(message) => {
|
||||
return failed(
|
||||
std::option::Option::Some("batch"),
|
||||
"malformed_token_batch",
|
||||
message,
|
||||
);
|
||||
},
|
||||
}
|
||||
} else {
|
||||
(parsed.parameters.clone(), std::vec::Vec::new())
|
||||
};
|
||||
let account_projection = project_accounts(parsed.code, accounts.as_slice());
|
||||
let suffix_length = bytes.len().saturating_sub(parsed.consumed);
|
||||
let authority = authority_projection(parsed.code, accounts.as_slice());
|
||||
let semantic_diagnostics =
|
||||
semantic_diagnostics(parsed.code, parameters.clone(), accounts.as_slice(), suffix_length);
|
||||
let committed = !input.transaction_failed;
|
||||
let parent = observation(
|
||||
input,
|
||||
parsed.code,
|
||||
parsed.tag,
|
||||
parameters,
|
||||
account_projection,
|
||||
authority,
|
||||
semantic_diagnostics,
|
||||
bytes.as_slice(),
|
||||
suffix_length,
|
||||
committed,
|
||||
format!("token:{}:0", parsed.code),
|
||||
input.instruction_path.clone(),
|
||||
event_family(parsed.code),
|
||||
);
|
||||
let mut observations = std::vec![parent];
|
||||
observations.extend(batch_children);
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Decoded,
|
||||
recognized_entry_code: std::option::Option::Some(parsed.code.to_string()),
|
||||
observations,
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_payload(input: &crate::CoreInstructionReplayInput) -> kb_core::Result<std::vec::Vec<u8>> {
|
||||
let payload = match input.instruction_payload_json.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"classic SPL Token instruction payload is not retained",
|
||||
));
|
||||
},
|
||||
};
|
||||
let encoded = match payload.get("dataBase64").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"classic SPL Token payload does not contain dataBase64",
|
||||
));
|
||||
},
|
||||
};
|
||||
let maximum_encoded = crate::SPL_TOKEN_MAX_INSTRUCTION_BYTES
|
||||
.saturating_mul(4)
|
||||
.saturating_div(3)
|
||||
.saturating_add(4);
|
||||
if encoded.len() > maximum_encoded {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"classic SPL Token base64 payload exceeds {maximum_encoded} bytes"
|
||||
)));
|
||||
}
|
||||
let decoded = match base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"classic SPL Token payload is not valid base64: {error}"
|
||||
)));
|
||||
},
|
||||
};
|
||||
if decoded.len() > crate::SPL_TOKEN_MAX_INSTRUCTION_BYTES {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"classic SPL Token payload exceeds {} decoded bytes",
|
||||
crate::SPL_TOKEN_MAX_INSTRUCTION_BYTES
|
||||
)));
|
||||
}
|
||||
return std::result::Result::Ok(decoded);
|
||||
}
|
||||
|
||||
fn parse_instruction(
|
||||
bytes: &[u8],
|
||||
nested_in_batch: bool,
|
||||
) -> std::result::Result<ParsedInstruction, std::string::String> {
|
||||
let tag = match bytes.first().copied() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err("empty instruction".to_string());
|
||||
},
|
||||
};
|
||||
let code = match crate::spl_token_entry_for_tag(tag) {
|
||||
std::option::Option::Some((_, value, _)) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(format!("unknown instruction tag {tag}"));
|
||||
},
|
||||
};
|
||||
if nested_in_batch && tag == 255 {
|
||||
return std::result::Result::Err("nested Batch instruction is forbidden".to_string());
|
||||
}
|
||||
let result = match tag {
|
||||
0 | 20 => parse_initialize_mint(bytes, code, tag),
|
||||
1 | 5 | 9 | 10 | 11 | 17 | 21 | 22 | 38 => std::result::Result::Ok(ParsedInstruction {
|
||||
code,
|
||||
tag,
|
||||
parameters: serde_json::json!({}),
|
||||
consumed: 1,
|
||||
}),
|
||||
255 => std::result::Result::Ok(ParsedInstruction {
|
||||
code,
|
||||
tag,
|
||||
parameters: serde_json::json!({}),
|
||||
consumed: bytes.len(),
|
||||
}),
|
||||
2 | 19 => parse_one_u8(bytes, code, tag, "m"),
|
||||
3 | 4 | 7 | 8 | 23 => parse_one_u64(bytes, code, tag, "amountRaw"),
|
||||
6 => parse_set_authority(bytes, code, tag),
|
||||
12 | 13 | 14 | 15 => parse_amount_decimals(bytes, code, tag),
|
||||
16 | 18 => parse_pubkey_parameter(bytes, code, tag, "owner"),
|
||||
24 => parse_ui_amount(bytes, code, tag),
|
||||
45 => parse_unwrap_lamports(bytes, code, tag),
|
||||
_ => std::result::Result::Err(format!("unsupported instruction tag {tag}")),
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
fn parse_initialize_mint(
|
||||
bytes: &[u8],
|
||||
code: &'static str,
|
||||
tag: u8,
|
||||
) -> std::result::Result<ParsedInstruction, std::string::String> {
|
||||
if bytes.len() < 35 {
|
||||
return std::result::Result::Err(format!("{code} requires at least 35 bytes"));
|
||||
}
|
||||
let mint_authority = pubkey_text(&bytes[2..34]);
|
||||
let (freeze_authority, option_length) = match pubkey_option(&bytes[34..]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(message) => return std::result::Result::Err(message),
|
||||
};
|
||||
return std::result::Result::Ok(ParsedInstruction {
|
||||
code,
|
||||
tag,
|
||||
parameters: serde_json::json!({
|
||||
"decimals": bytes[1],
|
||||
"mintAuthority": mint_authority,
|
||||
"freezeAuthority": freeze_authority,
|
||||
}),
|
||||
consumed: 34 + option_length,
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_one_u8(
|
||||
bytes: &[u8],
|
||||
code: &'static str,
|
||||
tag: u8,
|
||||
field: &str,
|
||||
) -> std::result::Result<ParsedInstruction, std::string::String> {
|
||||
if bytes.len() < 2 {
|
||||
return std::result::Result::Err(format!("{code} requires 2 bytes"));
|
||||
}
|
||||
return std::result::Result::Ok(ParsedInstruction {
|
||||
code,
|
||||
tag,
|
||||
parameters: one_parameter(field, serde_json::Value::from(bytes[1])),
|
||||
consumed: 2,
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_one_u64(
|
||||
bytes: &[u8],
|
||||
code: &'static str,
|
||||
tag: u8,
|
||||
field: &str,
|
||||
) -> std::result::Result<ParsedInstruction, std::string::String> {
|
||||
let amount = match read_u64(bytes, 1) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(message) => return std::result::Result::Err(message),
|
||||
};
|
||||
return std::result::Result::Ok(ParsedInstruction {
|
||||
code,
|
||||
tag,
|
||||
parameters: one_parameter(field, serde_json::Value::String(amount.to_string())),
|
||||
consumed: 9,
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_set_authority(
|
||||
bytes: &[u8],
|
||||
code: &'static str,
|
||||
tag: u8,
|
||||
) -> std::result::Result<ParsedInstruction, std::string::String> {
|
||||
if bytes.len() < 3 {
|
||||
return std::result::Result::Err("set_authority requires at least 3 bytes".to_string());
|
||||
}
|
||||
let authority_type = match bytes[1] {
|
||||
0 => "mint_tokens",
|
||||
1 => "freeze_account",
|
||||
2 => "account_owner",
|
||||
3 => "close_account",
|
||||
value => return std::result::Result::Err(format!("invalid authority type {value}")),
|
||||
};
|
||||
let (new_authority, option_length) = match pubkey_option(&bytes[2..]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(message) => return std::result::Result::Err(message),
|
||||
};
|
||||
return std::result::Result::Ok(ParsedInstruction {
|
||||
code,
|
||||
tag,
|
||||
parameters: serde_json::json!({
|
||||
"authorityType": authority_type,
|
||||
"newAuthority": new_authority,
|
||||
}),
|
||||
consumed: 2 + option_length,
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_amount_decimals(
|
||||
bytes: &[u8],
|
||||
code: &'static str,
|
||||
tag: u8,
|
||||
) -> std::result::Result<ParsedInstruction, std::string::String> {
|
||||
if bytes.len() < 10 {
|
||||
return std::result::Result::Err(format!("{code} requires 10 bytes"));
|
||||
}
|
||||
let amount = match read_u64(bytes, 1) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(message) => return std::result::Result::Err(message),
|
||||
};
|
||||
return std::result::Result::Ok(ParsedInstruction {
|
||||
code,
|
||||
tag,
|
||||
parameters: serde_json::json!({
|
||||
"amountRaw": amount.to_string(),
|
||||
"decimals": bytes[9],
|
||||
}),
|
||||
consumed: 10,
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_pubkey_parameter(
|
||||
bytes: &[u8],
|
||||
code: &'static str,
|
||||
tag: u8,
|
||||
field: &str,
|
||||
) -> std::result::Result<ParsedInstruction, std::string::String> {
|
||||
if bytes.len() < 33 {
|
||||
return std::result::Result::Err(format!("{code} requires 33 bytes"));
|
||||
}
|
||||
return std::result::Result::Ok(ParsedInstruction {
|
||||
code,
|
||||
tag,
|
||||
parameters: one_parameter(field, serde_json::Value::String(pubkey_text(&bytes[1..33]))),
|
||||
consumed: 33,
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_ui_amount(
|
||||
bytes: &[u8],
|
||||
code: &'static str,
|
||||
tag: u8,
|
||||
) -> std::result::Result<ParsedInstruction, std::string::String> {
|
||||
let text = match std::str::from_utf8(&bytes[1..]) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"ui amount is not UTF-8 at byte {}",
|
||||
error.valid_up_to()
|
||||
));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(ParsedInstruction {
|
||||
code,
|
||||
tag,
|
||||
parameters: serde_json::json!({"uiAmount": text}),
|
||||
consumed: bytes.len(),
|
||||
});
|
||||
}
|
||||
|
||||
fn parse_unwrap_lamports(
|
||||
bytes: &[u8],
|
||||
code: &'static str,
|
||||
tag: u8,
|
||||
) -> std::result::Result<ParsedInstruction, std::string::String> {
|
||||
if bytes.len() < 2 {
|
||||
return std::result::Result::Err("unwrap_lamports requires an option tag".to_string());
|
||||
}
|
||||
let (amount, consumed) = match bytes[1] {
|
||||
0 => (serde_json::Value::Null, 2),
|
||||
1 => {
|
||||
let value = match read_u64(bytes, 2) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(message) => return std::result::Result::Err(message),
|
||||
};
|
||||
(serde_json::Value::String(value.to_string()), 10)
|
||||
},
|
||||
value => return std::result::Result::Err(format!("invalid unwrap amount option {value}")),
|
||||
};
|
||||
return std::result::Result::Ok(ParsedInstruction {
|
||||
code,
|
||||
tag,
|
||||
parameters: serde_json::json!({"amountLamports": amount}),
|
||||
consumed,
|
||||
});
|
||||
}
|
||||
|
||||
fn read_u64(bytes: &[u8], offset: usize) -> std::result::Result<u64, std::string::String> {
|
||||
let slice = match bytes.get(offset..offset.saturating_add(8)) {
|
||||
std::option::Option::Some(value) if value.len() == 8 => value,
|
||||
_ => return std::result::Result::Err("truncated little-endian u64".to_string()),
|
||||
};
|
||||
let array = match <[u8; 8]>::try_from(slice) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_error) => {
|
||||
return std::result::Result::Err("invalid little-endian u64".to_string());
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok(u64::from_le_bytes(array));
|
||||
}
|
||||
|
||||
fn pubkey_option(
|
||||
bytes: &[u8],
|
||||
) -> std::result::Result<(serde_json::Value, usize), std::string::String> {
|
||||
return match bytes.first().copied() {
|
||||
std::option::Option::Some(0) => std::result::Result::Ok((serde_json::Value::Null, 1)),
|
||||
std::option::Option::Some(1) if bytes.len() >= 33 => {
|
||||
std::result::Result::Ok((serde_json::Value::String(pubkey_text(&bytes[1..33])), 33))
|
||||
},
|
||||
std::option::Option::Some(1) => {
|
||||
std::result::Result::Err("truncated optional pubkey".to_string())
|
||||
},
|
||||
std::option::Option::Some(value) => {
|
||||
std::result::Result::Err(format!("invalid optional pubkey tag {value}"))
|
||||
},
|
||||
std::option::Option::None => {
|
||||
std::result::Result::Err("missing optional pubkey tag".to_string())
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn pubkey_text(bytes: &[u8]) -> std::string::String {
|
||||
return bs58::encode(bytes).into_string();
|
||||
}
|
||||
|
||||
fn one_parameter(field: &str, value: serde_json::Value) -> serde_json::Value {
|
||||
let mut parameters = serde_json::Map::new();
|
||||
parameters.insert(field.to_string(), value);
|
||||
return serde_json::Value::Object(parameters);
|
||||
}
|
||||
|
||||
fn resolve_accounts(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
) -> kb_core::Result<std::vec::Vec<ResolvedAccount>> {
|
||||
let instruction_accounts = match input.instruction_accounts_json.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"classic SPL Token instruction accounts must be a JSON array",
|
||||
));
|
||||
},
|
||||
};
|
||||
let account_keys = match input.account_keys_json.as_array() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||
"classic SPL Token transaction account keys must be a JSON array",
|
||||
));
|
||||
},
|
||||
};
|
||||
let mut output = std::vec::Vec::with_capacity(instruction_accounts.len());
|
||||
for (position, account) in instruction_accounts.iter().enumerate() {
|
||||
let account_index = match account.get("accountIndex").and_then(serde_json::Value::as_u64) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"classic SPL Token instruction account {position} has no accountIndex"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let account_key = match account.get("accountKey").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) if !value.trim().is_empty() => value,
|
||||
_ => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"classic SPL Token instruction account {position} has no accountKey"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let resolved = account_keys.iter().find(|candidate| {
|
||||
return candidate.get("accountIndex").and_then(serde_json::Value::as_u64)
|
||||
== std::option::Option::Some(account_index);
|
||||
});
|
||||
let resolved = match resolved {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"classic SPL Token account index {account_index} is not resolved"
|
||||
)));
|
||||
},
|
||||
};
|
||||
if resolved.get("accountKey").and_then(serde_json::Value::as_str)
|
||||
!= std::option::Option::Some(account_key)
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"classic SPL Token account index {account_index} resolves to a different key"
|
||||
)));
|
||||
}
|
||||
let signer = match resolved.get("signer").and_then(serde_json::Value::as_bool) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"classic SPL Token account index {account_index} has no signer flag"
|
||||
)));
|
||||
},
|
||||
};
|
||||
let writable = match resolved.get("writable").and_then(serde_json::Value::as_bool) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||
"classic SPL Token account index {account_index} has no writable flag"
|
||||
)));
|
||||
},
|
||||
};
|
||||
output.push(ResolvedAccount {
|
||||
position,
|
||||
account_index,
|
||||
account_key: account_key.to_string(),
|
||||
signer,
|
||||
writable,
|
||||
source: resolved.get("source").cloned().unwrap_or(serde_json::Value::Null),
|
||||
});
|
||||
}
|
||||
return std::result::Result::Ok(output);
|
||||
}
|
||||
|
||||
fn project_accounts(code: &str, accounts: &[ResolvedAccount]) -> serde_json::Value {
|
||||
let authority_index = authority_index(code);
|
||||
let values = accounts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(role_position, account)| {
|
||||
return serde_json::json!({
|
||||
"position": account.position,
|
||||
"rolePosition": role_position,
|
||||
"accountIndex": account.account_index,
|
||||
"accountKey": account.account_key,
|
||||
"signer": account.signer,
|
||||
"writable": account.writable,
|
||||
"source": account.source,
|
||||
"role": account_role(code, role_position, authority_index),
|
||||
});
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
return serde_json::Value::Array(values);
|
||||
}
|
||||
|
||||
fn authority_projection(code: &str, accounts: &[ResolvedAccount]) -> serde_json::Value {
|
||||
let authority_index = authority_index(code);
|
||||
let index = match authority_index {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return serde_json::json!({
|
||||
"form": "not_applicable",
|
||||
"declaredAuthority": serde_json::Value::Null,
|
||||
"providedSigners": [],
|
||||
"statefulMultisigValidation": "not_applicable",
|
||||
});
|
||||
},
|
||||
};
|
||||
let authority = accounts.get(index);
|
||||
let provided_signers = accounts
|
||||
.iter()
|
||||
.skip(index.saturating_add(1))
|
||||
.filter(|account| return account.signer)
|
||||
.map(|account| return account.account_key.clone())
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let form = if accounts.len() > index.saturating_add(1) { "multisig" } else { "single" };
|
||||
return serde_json::json!({
|
||||
"form": form,
|
||||
"declaredAuthority": authority.map(|value| return value.account_key.clone()),
|
||||
"declaredAuthoritySigner": authority.map(|value| return value.signer),
|
||||
"providedSigners": provided_signers,
|
||||
"providedSignerCount": provided_signers.len(),
|
||||
"statefulMultisigValidation": if form == "multisig" {
|
||||
"requires_multisig_account_snapshot"
|
||||
} else {
|
||||
"not_applicable"
|
||||
},
|
||||
"membersClaimed": false,
|
||||
});
|
||||
}
|
||||
|
||||
fn authority_index(code: &str) -> std::option::Option<usize> {
|
||||
return match code {
|
||||
"transfer"
|
||||
| "approve"
|
||||
| "mint_to"
|
||||
| "burn"
|
||||
| "close_account"
|
||||
| "freeze_account"
|
||||
| "thaw_account"
|
||||
| "withdraw_excess_lamports"
|
||||
| "unwrap_lamports" => std::option::Option::Some(2),
|
||||
"revoke" | "set_authority" => std::option::Option::Some(1),
|
||||
"transfer_checked" | "approve_checked" => std::option::Option::Some(3),
|
||||
"mint_to_checked" | "burn_checked" => std::option::Option::Some(2),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
|
||||
fn account_role(
|
||||
code: &str,
|
||||
position: usize,
|
||||
authority_index: std::option::Option<usize>,
|
||||
) -> std::string::String {
|
||||
if authority_index.is_some_and(|index| return position > index) {
|
||||
return "multisig_signer".to_string();
|
||||
}
|
||||
let role = match (code, position) {
|
||||
("initialize_mint", 0) | ("initialize_mint2", 0) => "mint",
|
||||
("initialize_mint", 1) => "rent_sysvar",
|
||||
("initialize_account", 0) | ("initialize_account2", 0) | ("initialize_account3", 0) => {
|
||||
"token_account"
|
||||
},
|
||||
("initialize_account", 1) | ("initialize_account2", 1) | ("initialize_account3", 1) => {
|
||||
"mint"
|
||||
},
|
||||
("initialize_account", 2) => "owner",
|
||||
("initialize_account", 3) | ("initialize_account2", 2) => "rent_sysvar",
|
||||
("initialize_multisig", 0) | ("initialize_multisig2", 0) => "multisig_account",
|
||||
("initialize_multisig", 1) => "rent_sysvar",
|
||||
("initialize_multisig", _) | ("initialize_multisig2", _) => "multisig_member",
|
||||
("transfer", 0) | ("transfer_checked", 0) => "source_token_account",
|
||||
("transfer", 1) => "destination_token_account",
|
||||
("transfer_checked", 1) => "mint",
|
||||
("transfer_checked", 2) => "destination_token_account",
|
||||
("approve", 0) | ("approve_checked", 0) | ("revoke", 0) => "source_token_account",
|
||||
("approve", 1) | ("approve_checked", 2) => "delegate",
|
||||
("approve_checked", 1) => "mint",
|
||||
("mint_to", 0) | ("mint_to_checked", 0) => "mint",
|
||||
("mint_to", 1) | ("mint_to_checked", 1) => "destination_token_account",
|
||||
("burn", 0) | ("burn_checked", 0) => "source_token_account",
|
||||
("burn", 1) | ("burn_checked", 1) => "mint",
|
||||
("close_account", 0) | ("withdraw_excess_lamports", 0) | ("unwrap_lamports", 0) => {
|
||||
"source_token_account"
|
||||
},
|
||||
("close_account", 1) | ("withdraw_excess_lamports", 1) | ("unwrap_lamports", 1) => {
|
||||
"destination_account"
|
||||
},
|
||||
("freeze_account", 0) | ("thaw_account", 0) => "token_account",
|
||||
("freeze_account", 1) | ("thaw_account", 1) => "mint",
|
||||
("set_authority", 0) => "authority_target",
|
||||
("sync_native", 0) | ("initialize_immutable_owner", 0) => "token_account",
|
||||
("sync_native", 1) => "rent_sysvar",
|
||||
("get_account_data_size", 0) | ("amount_to_ui_amount", 0) | ("ui_amount_to_amount", 0) => {
|
||||
"mint"
|
||||
},
|
||||
(_, value) if authority_index == std::option::Option::Some(value) => "authority",
|
||||
_ => "unresolved",
|
||||
};
|
||||
return role.to_string();
|
||||
}
|
||||
|
||||
fn semantic_diagnostics(
|
||||
code: &str,
|
||||
parameters: serde_json::Value,
|
||||
accounts: &[ResolvedAccount],
|
||||
suffix_length: usize,
|
||||
) -> serde_json::Value {
|
||||
let mut values = std::vec::Vec::new();
|
||||
if suffix_length > 0 {
|
||||
values.push(serde_json::json!({
|
||||
"code": "ignored_wire_suffix",
|
||||
"lengthBytes": suffix_length,
|
||||
"runtimeSemantics": "official_interface_unpack_ignores_suffix_for_this_variant",
|
||||
}));
|
||||
}
|
||||
let valid_count = valid_account_count(code, accounts.len());
|
||||
if !valid_count {
|
||||
values.push(serde_json::json!({
|
||||
"code": "unexpected_account_count",
|
||||
"provided": accounts.len(),
|
||||
}));
|
||||
}
|
||||
if matches!(code, "initialize_multisig" | "initialize_multisig2") {
|
||||
let member_start = if code == "initialize_multisig" { 2 } else { 1 };
|
||||
let n = accounts.len().saturating_sub(member_start);
|
||||
let m = parameters.get("m").and_then(serde_json::Value::as_u64).unwrap_or(0);
|
||||
if !(1..=11).contains(&n) || m == 0 || m > n as u64 {
|
||||
values.push(serde_json::json!({
|
||||
"code": "invalid_multisig_threshold",
|
||||
"m": m,
|
||||
"n": n,
|
||||
"minSigners": 1,
|
||||
"maxSigners": 11,
|
||||
}));
|
||||
}
|
||||
}
|
||||
let authority_index = authority_index(code);
|
||||
if let std::option::Option::Some(index) = authority_index {
|
||||
if let std::option::Option::Some(authority) = accounts.get(index) {
|
||||
let multisig_form = accounts.len() > index.saturating_add(1);
|
||||
if !multisig_form && !authority.signer {
|
||||
values.push(serde_json::json!({"code":"missing_single_authority_signature"}));
|
||||
}
|
||||
if multisig_form {
|
||||
let invalid_signers = accounts
|
||||
.iter()
|
||||
.skip(index.saturating_add(1))
|
||||
.filter(|account| return !account.signer)
|
||||
.map(|account| return account.position)
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
if !invalid_signers.is_empty() {
|
||||
values.push(serde_json::json!({
|
||||
"code": "multisig_meta_missing_signature",
|
||||
"positions": invalid_signers,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return serde_json::Value::Array(values);
|
||||
}
|
||||
|
||||
fn valid_account_count(code: &str, count: usize) -> bool {
|
||||
return match code {
|
||||
"initialize_mint" => count == 2,
|
||||
"initialize_mint2" => count == 1,
|
||||
"initialize_account" => count == 4,
|
||||
"initialize_account2" => count == 3,
|
||||
"initialize_account3" => count == 2,
|
||||
"initialize_multisig" => (3..=13).contains(&count),
|
||||
"initialize_multisig2" => (2..=12).contains(&count),
|
||||
"transfer"
|
||||
| "approve"
|
||||
| "mint_to"
|
||||
| "burn"
|
||||
| "close_account"
|
||||
| "freeze_account"
|
||||
| "thaw_account"
|
||||
| "withdraw_excess_lamports"
|
||||
| "unwrap_lamports" => count >= 3,
|
||||
"revoke" | "set_authority" => count >= 2,
|
||||
"transfer_checked" | "approve_checked" => count >= 4,
|
||||
"mint_to_checked" | "burn_checked" => count >= 3,
|
||||
"sync_native" => count == 1 || count == 2,
|
||||
"get_account_data_size"
|
||||
| "initialize_immutable_owner"
|
||||
| "amount_to_ui_amount"
|
||||
| "ui_amount_to_amount" => count == 1,
|
||||
"batch" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn observation(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
code: &str,
|
||||
tag: u8,
|
||||
parameters: serde_json::Value,
|
||||
accounts: serde_json::Value,
|
||||
authority: serde_json::Value,
|
||||
semantic_diagnostics: serde_json::Value,
|
||||
wire: &[u8],
|
||||
suffix_length: usize,
|
||||
committed: bool,
|
||||
event_key: std::string::String,
|
||||
instruction_path: std::string::String,
|
||||
family: crate::EventFamily,
|
||||
) -> crate::DecodedObservation {
|
||||
let wire_hash = hash(wire);
|
||||
let event = crate::DecodedProtocolEvent {
|
||||
signature: crate::Signature(input.signature.clone()),
|
||||
slot: crate::Slot(input.slot),
|
||||
instruction_path: crate::InstructionPath(instruction_path.clone()),
|
||||
program_id: crate::ProgramId(input.program_id.clone()),
|
||||
protocol_code: crate::ProtocolCode(crate::SPL_TOKEN_SURFACE_CODE.to_string()),
|
||||
surface_code: crate::SurfaceCode(crate::SPL_TOKEN_SURFACE_CODE.to_string()),
|
||||
event_code: crate::EventCode(format!("{}.{}", crate::SPL_TOKEN_SURFACE_CODE, code)),
|
||||
event_name: crate::EventName(code.to_string()),
|
||||
event_family: family,
|
||||
source_kind: if input.instruction_path.contains('/') {
|
||||
crate::EventSourceKind::InnerInstruction
|
||||
} else {
|
||||
crate::EventSourceKind::Instruction
|
||||
},
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
};
|
||||
return crate::DecodedObservation {
|
||||
event_key,
|
||||
event,
|
||||
payload_json: serde_json::json!({
|
||||
"eventVersion": crate::SPL_TOKEN_EVENT_VERSION,
|
||||
"programId": input.program_id,
|
||||
"instruction": code,
|
||||
"wireTag": tag,
|
||||
"wireTagHex": format!("{tag:02x}"),
|
||||
"instructionPath": instruction_path,
|
||||
"instructionLocation": if input.instruction_path.contains('/') { "inner" } else { "outer" },
|
||||
"transactionSucceeded": !input.transaction_failed,
|
||||
"committed": committed,
|
||||
"parameters": parameters,
|
||||
"accounts": accounts,
|
||||
"authority": authority,
|
||||
"semanticDiagnostics": semantic_diagnostics,
|
||||
"wire": {
|
||||
"lengthBytes": wire.len(),
|
||||
"sha256": wire_hash,
|
||||
"prefixHex": hex_prefix(wire),
|
||||
"suffixLengthBytes": suffix_length,
|
||||
"complete": true,
|
||||
},
|
||||
"inference": {
|
||||
"mintFromCoreBalanceChanges": serde_json::Value::Null,
|
||||
"mintInvented": false,
|
||||
"rpcAccountReadUsed": false,
|
||||
},
|
||||
"returnData": {
|
||||
"availableInCoreReplayInput": false,
|
||||
"validated": false,
|
||||
},
|
||||
}),
|
||||
transaction_failed: input.transaction_failed,
|
||||
transaction_error: input.transaction_err_json.clone(),
|
||||
observation_committed: committed,
|
||||
proof: crate::DecoderProof {
|
||||
kind: crate::DecoderProofKind::Manual,
|
||||
confidence: crate::DecoderConfidence::ManualExact,
|
||||
evidence: std::vec![
|
||||
"spl_token_interface_3_0_0_wire_audit".to_string(),
|
||||
format!("wire_tag:{tag}"),
|
||||
format!("wire_sha256:{wire_hash}"),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn batch_observations(
|
||||
input: &crate::CoreInstructionReplayInput,
|
||||
bytes: &[u8],
|
||||
accounts: &[ResolvedAccount],
|
||||
) -> std::result::Result<
|
||||
(std::vec::Vec<crate::DecodedObservation>, serde_json::Value),
|
||||
std::string::String,
|
||||
> {
|
||||
if bytes.len() == 1 {
|
||||
return std::result::Result::Err("Batch requires at least one sub-instruction".to_string());
|
||||
}
|
||||
let mut cursor = 1_usize;
|
||||
let mut account_cursor = 0_usize;
|
||||
let mut index = 0_usize;
|
||||
let mut observations = std::vec::Vec::new();
|
||||
let mut manifest = std::vec::Vec::new();
|
||||
while cursor < bytes.len() {
|
||||
if index >= crate::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS {
|
||||
return std::result::Result::Err(format!(
|
||||
"Batch exceeds {} sub-instructions",
|
||||
crate::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS
|
||||
));
|
||||
}
|
||||
let account_count = match bytes.get(cursor).copied() {
|
||||
std::option::Option::Some(value) => value as usize,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(format!(
|
||||
"Batch entry {index} has no account count"
|
||||
));
|
||||
},
|
||||
};
|
||||
let data_length = match bytes.get(cursor.saturating_add(1)).copied() {
|
||||
std::option::Option::Some(value) => value as usize,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(format!("Batch entry {index} has no data length"));
|
||||
},
|
||||
};
|
||||
if data_length == 0 {
|
||||
return std::result::Result::Err(format!("Batch entry {index} has empty data"));
|
||||
}
|
||||
let data_start = cursor.saturating_add(2);
|
||||
let data_end = data_start.saturating_add(data_length);
|
||||
let child_wire = match bytes.get(data_start..data_end) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(format!(
|
||||
"Batch entry {index} data length exceeds payload"
|
||||
));
|
||||
},
|
||||
};
|
||||
let account_end = account_cursor.saturating_add(account_count);
|
||||
if account_end > crate::SPL_TOKEN_MAX_BATCH_ACCOUNTS || account_end > accounts.len() {
|
||||
return std::result::Result::Err(format!(
|
||||
"Batch entry {index} account slice exceeds provided accounts or bound"
|
||||
));
|
||||
}
|
||||
let parsed = match parse_instruction(child_wire, true) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(message) => {
|
||||
return std::result::Result::Err(format!("Batch entry {index}: {message}"));
|
||||
},
|
||||
};
|
||||
let child_accounts = &accounts[account_cursor..account_end];
|
||||
let suffix_length = child_wire.len().saturating_sub(parsed.consumed);
|
||||
observations.push(observation(
|
||||
input,
|
||||
parsed.code,
|
||||
parsed.tag,
|
||||
parsed.parameters.clone(),
|
||||
project_accounts(parsed.code, child_accounts),
|
||||
authority_projection(parsed.code, child_accounts),
|
||||
semantic_diagnostics(parsed.code, parsed.parameters, child_accounts, suffix_length),
|
||||
child_wire,
|
||||
suffix_length,
|
||||
!input.transaction_failed,
|
||||
format!("token:batch:{index}"),
|
||||
format!("{}/batch/{index}", input.instruction_path),
|
||||
event_family(parsed.code),
|
||||
));
|
||||
manifest.push(serde_json::json!({
|
||||
"position": index,
|
||||
"instruction": parsed.code,
|
||||
"wireTag": parsed.tag,
|
||||
"accountStart": account_cursor,
|
||||
"accountCount": account_count,
|
||||
"dataOffset": data_start,
|
||||
"dataLength": data_length,
|
||||
"derivedInstructionPath": format!("{}/batch/{index}", input.instruction_path),
|
||||
}));
|
||||
cursor = data_end;
|
||||
account_cursor = account_end;
|
||||
index = index.saturating_add(1);
|
||||
}
|
||||
if account_cursor != accounts.len() {
|
||||
return std::result::Result::Err(format!(
|
||||
"Batch consumed {account_cursor} of {} provided accounts",
|
||||
accounts.len()
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok((
|
||||
observations,
|
||||
serde_json::json!({
|
||||
"subInstructionCount": manifest.len(),
|
||||
"subInstructions": manifest,
|
||||
"nestedBatchAllowed": false,
|
||||
}),
|
||||
));
|
||||
}
|
||||
|
||||
fn event_family(code: &str) -> crate::EventFamily {
|
||||
return match code {
|
||||
"set_authority" | "initialize_multisig" | "initialize_multisig2" => {
|
||||
crate::EventFamily::Admin
|
||||
},
|
||||
"initialize_mint" | "initialize_mint2" | "mint_to" | "mint_to_checked" => {
|
||||
crate::EventFamily::TokenMint
|
||||
},
|
||||
"burn" | "burn_checked" => crate::EventFamily::TokenBurn,
|
||||
"batch" | "get_account_data_size" | "amount_to_ui_amount" | "ui_amount_to_amount" => {
|
||||
crate::EventFamily::Audit
|
||||
},
|
||||
_ => crate::EventFamily::TokenAccount,
|
||||
};
|
||||
}
|
||||
|
||||
fn hash(bytes: &[u8]) -> std::string::String {
|
||||
let digest = sha2::Sha256::digest(bytes);
|
||||
let mut output = std::string::String::with_capacity(64);
|
||||
for byte in digest {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fn hex_prefix(bytes: &[u8]) -> std::string::String {
|
||||
let length = std::cmp::min(bytes.len(), crate::SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES);
|
||||
let mut output = std::string::String::with_capacity(length.saturating_mul(2));
|
||||
for byte in &bytes[..length] {
|
||||
output.push_str(format!("{byte:02x}").as_str());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
fn failed(
|
||||
entry_code: std::option::Option<&str>,
|
||||
code: &str,
|
||||
message: std::string::String,
|
||||
) -> crate::DecoderExecutionResult {
|
||||
return crate::DecoderExecutionResult {
|
||||
status: crate::DecoderOutcomeStatus::Failed,
|
||||
recognized_entry_code: entry_code.map(str::to_string),
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: std::vec![crate::DecoderDiagnostic {
|
||||
code: code.to_string(),
|
||||
message,
|
||||
retriable: false,
|
||||
}],
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-lib/src/lib.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Consolidated decoder, executor, materializer and shared model library.
|
||||
#![warn(missing_docs)]
|
||||
@@ -27,6 +27,22 @@ pub(crate) use crate::decoder::SPL_MEMO_V1_SURFACE_CODE;
|
||||
pub(crate) use crate::decoder::SPL_MEMO_V3_SURFACE_CODE;
|
||||
/// Stable Memo v4 surface code.
|
||||
pub(crate) use crate::decoder::SPL_MEMO_V4_SURFACE_CODE;
|
||||
/// Maximum prefix retained in bounded classic SPL Token diagnostics.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// Current stable classic SPL Token decoded event contract version.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_EVENT_VERSION;
|
||||
/// One exact classic SPL Token instruction entry published by the official interface.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_INSTRUCTION_ENTRIES;
|
||||
/// Maximum total account metas consumed by one classic SPL Token batch.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_MAX_BATCH_ACCOUNTS;
|
||||
/// Maximum decoded sub-instructions in one classic SPL Token batch.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_MAX_BATCH_INSTRUCTIONS;
|
||||
/// Maximum retained classic SPL Token instruction payload size.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_MAX_INSTRUCTION_BYTES;
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_SURFACE_CODE;
|
||||
/// Canonical tracing target for the classic SPL Token decoder.
|
||||
pub(crate) use crate::decoder::SPL_TOKEN_TRACING_TARGET;
|
||||
/// Stable Address Lookup Table surface code.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE;
|
||||
/// Stable deprecated immutable BPF Loader surface code.
|
||||
@@ -79,6 +95,12 @@ pub(crate) use crate::decoder::solana::solana_core_config_recognize;
|
||||
pub(crate) use crate::decoder::solana::solana_core_resolve_accounts;
|
||||
/// Decodes one bounded SPL Memo instruction.
|
||||
pub(crate) use crate::decoder::spl_memo_decode;
|
||||
/// Decodes one bounded classic SPL Token instruction.
|
||||
pub(crate) use crate::decoder::spl_token_decode;
|
||||
/// Returns the published entry matching one classic SPL Token tag.
|
||||
pub(crate) use crate::decoder::spl_token_entry_for_tag;
|
||||
/// Returns the first byte of one retained classic SPL Token payload.
|
||||
pub(crate) use crate::decoder::spl_token_payload_tag;
|
||||
|
||||
/// Stable protocol code shared by native Solana events.
|
||||
pub(crate) use crate::decoder::solana::SOLANA_CORE_PROTOCOL_CODE;
|
||||
@@ -204,6 +226,8 @@ pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_recognize;
|
||||
|
||||
/// Exact decoder for the three registered SPL Memo generations.
|
||||
pub use crate::decoder::SplMemoDecoder;
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
pub use crate::decoder::SplTokenDecoder;
|
||||
/// Current contextual core instruction input contract version.
|
||||
pub use crate::decoder::api::contracts::CORE_INSTRUCTION_INPUT_CONTRACT_VERSION;
|
||||
/// Stable contextual decoded observation.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: kb-program-ids/README.md -->
|
||||
<!-- version: 2 -->
|
||||
<!-- version: 3 -->
|
||||
|
||||
# kb-program-ids
|
||||
|
||||
`kb-program-ids` est la source unique des identifiants de programmes Solana et des comptes natifs connus utilisés par le workspace.
|
||||
|
||||
La tranche `0.1.0-pre.004` expose les 18 surfaces exécutables nécessaires au décodeur Solana Core, les comptes natifs associés utilisés par leurs contrats et l’identifiant SPL Token classique employé par les tests de frontière. La tranche `0.1.0-pre.005` ajoute les trois identifiants SPL Memo v1, v3 et v4 sans les classer parmi les surfaces natives. Les identifiants SPL et protocolaires supplémentaires seront ajoutés avec leurs tranches de migration respectives.
|
||||
La tranche `0.1.0-pre.004` expose les 18 surfaces exécutables nécessaires au décodeur Solana Core, les comptes natifs associés utilisés par leurs contrats et l’identifiant SPL Token classique employé par les tests de frontière. La tranche `0.1.0-pre.005` ajoute les trois identifiants SPL Memo v1, v3 et v4 sans les classer parmi les surfaces natives. `0.1.0-pre.006` active l’identifiant SPL Token classique dans un décodeur concret tout en le maintenant hors des surfaces natives. Les identifiants SPL et protocolaires supplémentaires seront ajoutés avec leurs tranches de migration respectives.
|
||||
|
||||
## API publique
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: kb-program-ids/src/lib.rs
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -195,13 +195,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spl_memo_generations_are_registered_without_joining_native_surfaces() {
|
||||
let memo_program_ids = [
|
||||
fn spl_primitives_are_registered_without_joining_native_surfaces() {
|
||||
let spl_program_ids = [
|
||||
crate::SPL_MEMO_V1_PROGRAM_ID,
|
||||
crate::SPL_MEMO_V3_PROGRAM_ID,
|
||||
crate::SPL_MEMO_V4_PROGRAM_ID,
|
||||
crate::SPL_TOKEN_PROGRAM_ID,
|
||||
];
|
||||
for program_id in memo_program_ids {
|
||||
for program_id in spl_program_ids {
|
||||
assert!(crate::find_registered_program_id(program_id).is_some());
|
||||
assert!(
|
||||
!crate::native_program_ids()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# file: scripts/audit_khadhroony_workspace_rules.py
|
||||
# version: 2
|
||||
# version: 3
|
||||
|
||||
"""Audit mechanically verifiable rules specific to khadhroony-bot3."""
|
||||
|
||||
@@ -161,6 +161,69 @@ def audit_solana_types(root: pathlib.Path) -> list[Violation]:
|
||||
return violations
|
||||
|
||||
|
||||
def audit_wincode_resolution(root: pathlib.Path) -> list[Violation]:
|
||||
"""Require the currently compatible Solana wincode dependency family."""
|
||||
|
||||
violations: list[Violation] = []
|
||||
cargo_path = root / "Cargo.toml"
|
||||
lock_path = root / "Cargo.lock"
|
||||
if not cargo_path.exists():
|
||||
return violations
|
||||
cargo = tomllib.loads(cargo_path.read_text(encoding="utf-8"))
|
||||
workspace_wincode = cargo.get("workspace", {}).get("dependencies", {}).get("wincode")
|
||||
version = workspace_wincode.get("version") if isinstance(workspace_wincode, dict) else None
|
||||
if version != "^0.5":
|
||||
violations.append(
|
||||
Violation(
|
||||
"KH_DEP001",
|
||||
"Cargo.toml",
|
||||
1,
|
||||
"workspace wincode must remain constrained to `^0.5`",
|
||||
)
|
||||
)
|
||||
if not lock_path.exists():
|
||||
violations.append(
|
||||
Violation(
|
||||
"KH_DEP002",
|
||||
"Cargo.lock",
|
||||
1,
|
||||
"versioned application workspace lockfile is required",
|
||||
)
|
||||
)
|
||||
return violations
|
||||
lock = tomllib.loads(lock_path.read_text(encoding="utf-8"))
|
||||
packages = lock.get("package", [])
|
||||
resolved_wincode = [
|
||||
package.get("version")
|
||||
for package in packages
|
||||
if package.get("name") == "wincode"
|
||||
]
|
||||
resolved_varint = [
|
||||
package.get("version")
|
||||
for package in packages
|
||||
if package.get("name") == "solana-wincode-varint"
|
||||
]
|
||||
if resolved_wincode != ["0.5.5"]:
|
||||
violations.append(
|
||||
Violation(
|
||||
"KH_DEP003",
|
||||
"Cargo.lock",
|
||||
1,
|
||||
f"expected only wincode 0.5.5, resolved {resolved_wincode}",
|
||||
)
|
||||
)
|
||||
if resolved_varint != ["1.0.0"]:
|
||||
violations.append(
|
||||
Violation(
|
||||
"KH_DEP004",
|
||||
"Cargo.lock",
|
||||
1,
|
||||
f"expected only solana-wincode-varint 1.0.0, resolved {resolved_varint}",
|
||||
)
|
||||
)
|
||||
return violations
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Run the khadhroony-specific audit."""
|
||||
|
||||
@@ -169,7 +232,12 @@ def main() -> int:
|
||||
parser.add_argument("--report-only", action="store_true")
|
||||
arguments = parser.parse_args()
|
||||
root = pathlib.Path(arguments.root).resolve()
|
||||
violations = audit_tracing(root) + audit_kb_lib_tracing(root) + audit_solana_types(root)
|
||||
violations = (
|
||||
audit_tracing(root)
|
||||
+ audit_kb_lib_tracing(root)
|
||||
+ audit_solana_types(root)
|
||||
+ audit_wincode_resolution(root)
|
||||
)
|
||||
violations.sort(key=lambda item: (item.code, item.path, item.line, item.message))
|
||||
if not violations:
|
||||
sys.stdout.write("Khadhroony workspace rule audit: clean\n")
|
||||
|
||||
Reference in New Issue
Block a user