From 4187643148d981be9dcff4682f69e5db7fdbcc67 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Wed, 5 Aug 2026 19:36:17 +0200 Subject: [PATCH] v0.4.8-pre.005 --- Cargo.toml | 4 +- docs/IDL_AUDIT.md | 8 +- docs/IDL_TO_KB_LIB_NOMENCLATURE.md | 188 +-- docs/README.md | 7 +- ...GRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md | 152 ++ ...TADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md | 36 +- kb-lib/CHANGELOG.md | 14 +- kb-lib/README.md | 15 +- kb-lib/USAGE.md | 21 +- kb-lib/src/decoder.rs | 53 +- kb-lib/src/decoder/metadata.rs | 48 +- .../metadata/solana_program_metadata.rs | 86 +- .../solana_program_metadata/constants.rs | 330 ++++- .../solana_program_metadata/decoder.rs | 134 ++ .../solana_program_metadata/instruction.rs | 1282 +++++++++++++++++ kb-lib/src/lib.rs | 47 +- ...solana_program_metadata_instruction_api.rs | 81 ++ ...A_PROGRAM_METADATA_INSTRUCTION_MATRIX.json | 111 ++ 18 files changed, 2448 insertions(+), 169 deletions(-) create mode 100644 docs/audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md create mode 100644 kb-lib/src/decoder/metadata/solana_program_metadata/decoder.rs create mode 100644 kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs create mode 100644 kb-lib/tests/external_solana_program_metadata_instruction_api.rs create mode 100644 test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json diff --git a/Cargo.toml b/Cargo.toml index ef5a83d..067fb8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ # file: Cargo.toml -# version: 26 +# version: 27 [workspace] resolver = "3" @@ -18,7 +18,7 @@ members = [ ] [workspace.package] -version = "0.4.8-pre.4" +version = "0.4.8-pre.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3" diff --git a/docs/IDL_AUDIT.md b/docs/IDL_AUDIT.md index 4d4ba58..f1a0c39 100644 --- a/docs/IDL_AUDIT.md +++ b/docs/IDL_AUDIT.md @@ -1,5 +1,5 @@ - + # Audit des IDL v3 @@ -12,6 +12,12 @@ La copie officielle `metadata.ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S.solana_program_metadata.V0_0_0.from_github_solana_program.json` a été réauditée contre l’IDL Codama publiée par `solana-program/program-metadata`. Le Program ID, la version déclarée `0.0.0`, les trois PDA, les deux comptes, les neuf instructions, les types et les cinq erreurs concordent. Le JSON local reste inchangé et conserve l’empreinte inventoriée. Voir `audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md`. +## Réconciliation runtime Solana Program Metadata — `0.4.8-pre.005` + +L’IDL conserve exactement neuf instructions et reste inchangée. L’audit des tags stables `program@v1.0.0` et `program@v1.0.1` confirme le même inventaire `0..8`. Le processeur actuel accepte toutefois quelques formes supplémentaires par rapport au client Codama : `SetData` peut omettre `data_source` et préserver les données existantes, tandis que `SetImmutable`, `Trim` et `Close` ignorent un suffixe wire. Ces formes sont couvertes comme compatibilité runtime, sans modifier ni réécrire l’IDL. + +Le tag `5` a porté le nom de travail `WithdrawExcessLamports` avant la première release stable, puis a été remplacé par `Trim` sur le même discriminant. Il ne constitue pas une instruction historique distincte dans l’inventaire IDL. Voir `audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md`. + ## Inventaire | Nom v3 | Program ID déclaré | Version déclarée | Instructions | SHA-256 | diff --git a/docs/IDL_TO_KB_LIB_NOMENCLATURE.md b/docs/IDL_TO_KB_LIB_NOMENCLATURE.md index 691d1f3..50ed619 100644 --- a/docs/IDL_TO_KB_LIB_NOMENCLATURE.md +++ b/docs/IDL_TO_KB_LIB_NOMENCLATURE.md @@ -1,101 +1,101 @@ - + # Correspondance IDL ↔ `kb-lib` Ce tableau constitue la cible de classification pour les futurs décodeurs et exécuteurs. Il ne signifie pas que tous les modules proposés sont déjà implémentés. -| IDL v3 | Module actuel | Module cible | Action Rust | -|-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------|------------------------------------------------|---------------------------------------------------------------------| -| `adapter.DecZY86MU5Gj7kppfUCEmd4LbXXuyZH1yHaP2NTqdiZB.saber_decimal_wrapper.V0_0_0.from_solscan.json` | `adapter/saber_decimal_wrapper` | `adapter/saber_decimal_wrapper` | Aucune | -| `adapter.TwRapQCDhWkZRrDaHfZGuHxkZ91gHDRkyuzNqeU5MgR.spl_token_wrap.V0_1_0.from_github_solana_program.json` | `—` | `adapter/spl_token_wrap` | Créer `adapter/spl_token_wrap` | -| `amm.2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c.lifinity_swap_v2.V0_1_1.from_solscan.json` | `amm/lifinity_swap_v2` | `amm/lifinity_swap_v2` | Aucune | -| `amm.AMMJdEiCCa8mdugg6JPF7gFirmmxisTfDJoSNSUi5zDJ.metadao_v0_5.V0_5_0.from_solscan.json` | `amm/metadao_v0_5` | `amm/metadao_v0_5` | Aucune | -| `amm.BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p.bonk_swap.V0_1_1.from_solscan.json` | `amm/bonk_swap` | `amm/bonk_swap` | Aucune | -| `amm.Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB.meteora_damm_v1.V0_5_2.from_solscan.json` | `amm/meteora_damm_v1` | `amm/meteora_damm_v1` | Aucune | -| `amm.GAMMA7meSFWaBXF25oSUgmGRwaW6sCMFLmBNiMSdbHVT.goosefx_gamma.V0_2_0.from_solscan.json` | `amm/goosefx_gamma` | `amm/goosefx_gamma` | Aucune | -| `amm.GFXsSL5sSaDfNFQUYsHekbWBW1TsFdjDYzACh62tEHxn.goosefx_v2.V0_3_0.from_solscan.json` | `amm/goosefx_v2` | `amm/goosefx_v2` | Aucune | -| `amm.Gswppe6ERWKpUTXvRPfXdzHhiCyJvLadVvXGfdpBqcE1.guac_swap.V0_1_1.from_solscan.json` | `amm/guac_swap` | `amm/guac_swap` | Aucune | -| `amm.WooFif76YGRNjk1pA8wCsN67aQsD9f9iLsz4NcJ1AVb.woofi.V0_1_0.from_solscan.json` | `amm/woofi` | `amm/woofi` | Aucune | -| `amm.cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG.meteora_damm_v2.V0_2_0.from_solscan.json` | `amm/meteora_damm_v2` | `amm/meteora_damm_v2` | Aucune | -| `amm.pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA.pump_swap.V0_1_0.from_solscan.json` | `amm/pump_swap` | `amm/pump_swap` | Aucune | -| `amm.vrTGoBuy5rYSxAfV3jaRJWHH6nN9WK4NRExGxsk1bCJ.vertigo.V0_1_0.from_solscan.json` | `amm/vertigo` | `amm/vertigo` | Aucune | -| `bridge.6doghB248px58JSSwG4qejQ46kFMW4AMj7vzJnWZHNZn.layer_zero_executor.V0_1_0.from_solscan.json` | `bridge/layer_zero_executor` | `bridge/layer_zero_executor` | Aucune | -| `bridge.76y77prsiCMvXMjuoZ5VRrhG5qYBrUMYTE5WgHqgjEn6.layer_zero_endpoint.V0_1_0.from_solscan.json` | `bridge/layer_zero_endpoint` | `bridge/layer_zero_endpoint` | Aucune | -| `bridge.CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe.circle_cctp_token_messenger_minter_v2.V0_2_0.from_solscan.json` | `bridge/circle_cctp_token_messenger_minter_v2` | `bridge/circle_cctp_token_messenger_minter_v2` | Aucune | -| `bridge.CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3.circle_cctp_token_messenger_minter.V0_1_0.from_solscan.json` | `bridge/circle_cctp_token_messenger_minter` | `bridge/circle_cctp_token_messenger_minter` | Aucune | -| `clmm.6dMXqGZ3ga2dikrYS9ovDXgHGh5RUsb2RTUj6hrQXhk6.stabble.V0_1_0.from_solscan.json` | `clmm/stabble` | `clmm/stabble` | Aucune | -| `clmm.CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK.raydium.V0_1_0.from_solscan.json` | `clmm/raydium` | `clmm/raydium` | Aucune | -| `clmm.HpNfyc2Saw7RKkQd8nEL4khUcuPhQ7WwY1B2qjx8jxFq.pancake_swap.V0_1_0.from_solscan.json` | `clmm/pancake_swap` | `clmm/pancake_swap` | Aucune | -| `clmm.REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2.byreal.V0_1_0.from_solscan.json` | `clmm/byreal` | `clmm/byreal` | Aucune | -| `clmm.fUSioN9YKKSa3CUC2YUc4tPkHJ5Y6XW1yz8y6F7qWz9.fusion.V1_1_4.from_solscan.json` | `clmm/fusion` | `clmm/fusion` | Aucune | -| `clmm.whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc.orca_whirlpool.V0_9_0.from_solscan.json` | `clmm/orca_whirlpool` | `clmm/orca_whirlpool` | Aucune | -| `cpmm.CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C.raydium.V0_2_0.from_solscan.json` | `cpmm/raydium` | `cpmm/raydium` | Aucune | -| `dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.meteora.V0_12_0.from_solscan.json` | `dlmm/meteora` | `dlmm/meteora` | Aucune | -| `fees.pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ.pump_fees.V0_1_0.from_solscan.json` | `admin/pump_fees` | `fees/pump_fees` | Déplacer `admin/pump_fees` → `fees/pump_fees` | -| `governance.FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq.metadao_futarchy.V0_6_1.from_solscan.json` | `amm/metadao_futarchy_amm` | `governance/metadao_futarchy` | Déplacer `amm/metadao_futarchy_amm` → `governance/metadao_futarchy` | -| `governance.SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf.squads_multisig.V2_0_0.from_solscan.json` | `—` | `governance/squads_multisig` | Créer `governance/squads_multisig` | -| `governance.WALL8ucBuUyL46QYxwYJjidaFYhdvxUFrgvBxPshERx.metadao_bid_wall.V0_7_0.from_solscan.json` | `governance/metadao_bid_wall` | `governance/metadao_bid_wall` | Aucune | -| `launchpad.5U3EU2ubXtK84QcRjWVmYt9RaDyA8gKxdUrPFXmZyaki.virtuals.V0_1_0.from_solscan.json` | `amm/virtuals` | `launchpad/virtuals` | Déplacer `amm/virtuals` → `launchpad/virtuals` | -| `launchpad.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.pump_fun.V0_1_0.from_solscan.json` | `launchpad/pump_fun` | `launchpad/pump_fun` | Aucune | -| `launchpad.LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj.raydium_launchlab.V0_2_0.from_solscan.json` | `launchpad/raydium_launchlab` | `launchpad/raydium_launchlab` | Aucune | -| `launchpad.MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG.moonit.V0_1_0.from_solscan.json` | `launchpad/moonit` | `launchpad/moonit` | Aucune | -| `launchpad.PdMDrKEMaX8q7CCJb7NvUCxerBCcsFUa4LjBEynTtEd.pump_pumpup_ai.V0_1_0.from_solscan.json` | `launchpad/pump_pumpup_ai` | `launchpad/pump_pumpup_ai` | Aucune | -| `launchpad.T8HsGYv7sMk3kTnyaRqZrbRPuntYzdh12evXBkprint.printr.V1_0_0.from_solscan.json` | `launchpad/printr` | `launchpad/printr` | Aucune | -| `launchpad.boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4.boop_fun.V0_3_0.from_solscan.json` | `launchpad/boop_fun` | `launchpad/boop_fun` | Aucune | -| `launchpad.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.meteora_dbc.V0_1_10.from_solscan.json` | `launchpad/meteora_dbc` | `launchpad/meteora_dbc` | Aucune | -| `launchpad.moontUzsdepotRGe5xsfip7vLPTJnVuafqdUWexVnPM.metadao_ico.V0_7_0.from_solscan.json` | `launchpad/metadao_ico` | `launchpad/metadao_ico` | Aucune | -| `launchpad.waveQX2yP3H1pVU8djGvEHmYg8uamQ84AuyGtpsrXTF.orca_wavebreak.V1_1_5.from_solscan.json` | `launchpad/orca_wavebreak` | `launchpad/orca_wavebreak` | Aucune | -| `lending.C1onEW2kPetmHmwe74YC1ESx3LnFEpVau6g2pg4fHycr.clone.V0_1_0.from_solscan.json` | `lending/clone` | `lending/clone` | Aucune | -| `lending.KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD.kamino.V1_23_0.from_solscan.json` | `lending/kamino` | `lending/kamino` | Aucune | -| `lending.MFv2hWf31Z9kbCa1snEPYctwafyhdvnV7FZnsebVacA.marginfi_v2.V0_1_8.from_solscan.json` | `lending/marginfi_v2` | `lending/marginfi_v2` | Aucune | -| `lock.LockrWmn6K5twhz3y9w1dQERbmgSaRkfnTeTKbpofwE.raydium_lp.V0_1_0.from_solscan.json` | `lock/raydium_lp` | `lock/raydium_lp` | Aucune | -| `metadata.ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S.solana_program_metadata.V0_0_0.from_github_solana_program.json` | `metadata/solana_program_metadata` | `metadata/solana_program_metadata` | Modèles wire et décodeurs de comptes présents ; implémenter les instructions, la matérialisation et l’exécuteur | -| `metadata.metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s.metaplex_token_metadata.V1_14_0.from_solscan.json` | `metadata/metaplex_token_metadata` | `metadata/metaplex_token_metadata` | Aucune | -| `nft.BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY.metaplex_bubblegum.V0_12_0.from_solscan.json` | `nft/metaplex_bubblegum` | `nft/metaplex_bubblegum` | Aucune | -| `nft.TCMPhJdwDryooaGtiocG1u3xcYbRpiJzb283XfCZsDp.tensor_cnft.V2_0_1.from_solscan.json` | `—` | `nft/tensor_cnft` | Créer `nft/tensor_cnft` | -| `orderbook.j1o2qRpjcyUwEvwtcfhEQefh773ZgjxcVRry7LDqg5X.jupiter_limit_order_v2.V0_1_0.from_solscan.json` | `orderbook/jupiter_limit_order_v2` | `orderbook/jupiter_limit_order_v2` | Aucune | -| `orderbook.jupoNjAxXgZ4rjzxzPMP4oxduvQsQtZzyknqvzYNrNu.jupiter_limit_order.V0_1_0.from_solscan.json` | `orderbook/jupiter_limit_order` | `orderbook/jupiter_limit_order` | Aucune | -| `orderbook.opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb.openbook_v2.V0_1_0.from_solscan.json` | `orderbook/openbook_v2` | `orderbook/openbook_v2` | Aucune | -| `perpetuals.EtrnLzgbS7nMMy5fbD42kXiUzGg8XQzJ972Xtk1cjWih.phoenix_eternal.V0_2_0.from_solscan.json` | `—` | `perpetuals/phoenix_eternal` | Créer `perpetuals/phoenix_eternal` | -| `perpetuals.PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu.jupiter.V0_1_0.from_solscan.json` | `perpetuals/jupiter` | `perpetuals/jupiter` | Aucune | -| `perpetuals.ZETAxsqBRek56DhiGXrn75yj2NHU3aYUnxvHXpkf3aD.zeta.V0_1_0.from_solscan.json` | `perpetuals/zeta` | `perpetuals/zeta` | Aucune | -| `perpetuals.dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH.drift_v2.V2_150_0.from_solscan.json` | `perpetuals/drift_v2` | `perpetuals/drift_v2` | Aucune | -| `router.6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma.okx_labs_v1.V0_1_0.from_solscan.json` | `router/okx_labs_v1` | `router/okx_labs_v1` | Aucune | -| `router.DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH.dflow_aggregator_v4.V0_1_0.from_solscan.json` | `router/dflow_aggregator_v4` | `router/dflow_aggregator_v4` | Aucune | -| `router.JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB.jupiter_aggregator_v4.V0_1_0.from_solscan.json` | `router/jupiter_aggregator_v4` | `router/jupiter_aggregator_v4` | Aucune | -| `router.JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4.jupiter_aggregator_v6.V0_1_0.from_solscan.json` | `router/jupiter_aggregator_v6` | `router/jupiter_aggregator_v6` | Aucune | -| `router.proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u.okx_labs_v2.V0_1_0.from_solscan.json` | `router/okx_labs_v2` | `router/okx_labs_v2` | Aucune | -| `rwa.XzTT4XB8m7sLD2xi6snefSasaswsKCxx5Tifjondogm.ondo_global_markets.V0_1_0.from_solscan.json` | `rwa/ondo_global_markets` | `rwa/ondo_global_markets` | Aucune | -| `solana.11111111111111111111111111111111.core.system.V0_0_1.from_github_solana_program.json` | `solana/core/system` | `solana/core/system` | Aucune | -| `solana.AddressLookupTab1e1111111111111111111111111.core.address_lookup_table.V0_0_1.from_github_solana_program.json` | `solana/core/address_lookup_table` | `solana/core/address_lookup_table` | Aucune | -| `solana.BPFLoaderUpgradeab1e11111111111111111111111.core.loader_v3.V0_0_0.from_github_solana_program.json` | `solana/core/loader_v3` | `solana/core/loader_v3` | Aucune | -| `solana.ComputeBudget111111111111111111111111111111.core.compute_budget.V0_0_1.from_github_solana_program.json` | `solana/core/compute_budget` | `solana/core/compute_budget` | Aucune | -| `solana.Config1111111111111111111111111111111111111.core.config.V0_0_1.from_github_solana_program.json` | `solana/core/config` | `solana/core/config` | Aucune | -| `solana.Feature111111111111111111111111111111111111.core.feature.V0_0_1.from_github_solana_program.json` | `solana/core/feature` | `solana/core/feature` | Aucune | -| `solana.LoaderV411111111111111111111111111111111111.core.loader_v4.V0_0_0.from_github_solana_program.json` | `solana/core/loader_v4` | `solana/core/loader_v4` | Aucune | -| `solana.Stake11111111111111111111111111111111111111.core.stake.V4_3_1.from_github_solana_program.json` | `solana/core/stake` | `solana/core/stake` | Aucune | -| `spl.MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr.memo.v3.V3_0_1.from_github_solana_program.json` | `spl/memo/v3` | `spl/memo/v3` | Aucune | -| `spl.TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.token.V3_3_0.from_github_solana_program.json` | `spl/token` | `spl/token` | Aucune | -| `spl.TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.token_2022.V3_0_1.from_github_solana_program.json` | `spl/token_2022` | `spl/token_2022` | Aucune | -| `stable.HYEXCHtHkBagdStcJCp3xbbb9B7sdMdWXFNj6mdsG4hn.swap_hylo_exchange.V0_1_4.from_solscan.json` | `stable/swap_hylo_exchange` | `stable/swap_hylo_exchange` | Aucune | -| `stable.JUPUSDecMzAVgztLe6eGhwUBj1Pn3j9WAXwmtHmfbRr.swap_jupiter_stable.V0_1_0.from_solscan.json` | `stable/swap_jupiter_stable` | `stable/swap_jupiter_stable` | Aucune | -| `stable.NUMERUNsFCP3kuNmWZuXtm1AaQCPj9uw6Guv2Ekoi5P.swap_numeraire.V0_1_0.from_solscan.json` | `stable/swap_numeraire` | `stable/swap_numeraire` | Aucune | -| `stable.swapNyd8XiQwJ6ianp9snpu4brUqFxadzvHebnAXjJZ.swap_stabble.V1_5_0.from_solscan.json` | `stable/swap_stabble` | `stable/swap_stabble` | Aucune | -| `staking.4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7.jito_tip_distribution.V0_1_5.from_solscan.json` | `—` | `staking/jito_tip_distribution` | Créer `staking/jito_tip_distribution` | -| `staking.FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr.kamino_farm.V1_6_5.from_solscan.json` | `staking/kamino_farm` | `staking/kamino_farm` | Aucune | -| `staking.MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD.marinade_finance.V0_1_0.from_solscan.json` | `staking/marinade_finance` | `staking/marinade_finance` | Aucune | -| `storage.recr1L3PCGKLbckBqMNcJhuuyU1zgo8nBhfLVsJNwr5.solana_record.V0_4_0.from_github_solana_program.json` | `—` | `storage/solana_record` | Créer `storage/solana_record` | -| `strategy.DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M.jupiter_dca.V0_1_0.from_solscan.json` | `strategy/jupiter_dca` | `strategy/jupiter_dca` | Déplacer `strategy/jupiter_dca` → `strategy/jupiter_dca` | -| `treasury.treaf4wWBBty3fHdyBpo35Mz84M8k3heKXmjmi9vFt5.helium_treasury_management.V0_2_3.from_solscan.json` | `treasury/helium_treasury_management` | `treasury/helium_treasury_management` | Aucune | -| `vault.24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi.meteora.V0_7_2.from_solscan.json` | `vault/meteora` | `vault/meteora` | Aucune | -| `vault.6LtLpnUFNByNXLyCoK9wA2MykKAmQNZKBdY8s47dehDc.kamino_yvaults.V0_1_0.from_solscan.json` | `vault/kamino_yvaults` | `vault/kamino_yvaults` | Aucune | -| `vault.CarrotwivhMpDnm27EHmRLeQ683Z1PufuqEmBZvD282s.carrot_defi.V0_1_0.from_solscan.json` | `vault/carrot_defi` | `vault/carrot_defi` | Aucune | -| `vault.HysTabVUfmQBFcmzu1ctRd1Y1fxd66RBpboy1bmtDSQQ.hylo_stability_pool.V0_1_4.from_solscan.json` | `vault/hylo_stability_pool` | `vault/hylo_stability_pool` | Aucune | -| `vault.KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd.kamino_v2.V2_1_1.from_solscan.json` | `vault/kamino_v2` | `vault/kamino_v2` | Aucune | -| `vault.kvauTFR8qm1dhniz6pYuBZkuene3Hfrs1VQhVRgCNrr.kamino.V0_1_0.from_solscan.json` | `vault/kamino` | `vault/kamino` | Aucune | -| `vesting.LocpQgucEQHbqNABEYvBvwoxCPsSbG91A1QaQhQQqjn.jupiter_lock.V0_4_0.from_solscan.json` | `admin/jupiter_lock` | `vesting/jupiter_lock` | Déplacer `admin/jupiter_lock` → `vesting/jupiter_lock` | -| `vesting.strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m.streamflow.V0_1_0.from_solscan.json` | `vesting/streamflow` | `vesting/streamflow` | Aucune | -| `wallet.JSW99DKmxNyREQM14SQLDykeBvEUG63TeohrvmofEiw.jupiter_apepro_smart_wallet.V0_1_0.from_solscan.json` | `wallet/jupiter_apepro_smart_wallet` | `wallet/jupiter_apepro_smart_wallet` | Aucune | -| `weighted.swapFpHZwjELNnjvThjajtiVmkz3yPQEHjLtka2fwHW.swap_stabble.V1_5_0.from_solscan.json` | `weighted/swap_stabble` | `weighted/swap_stabble` | Aucune | +| IDL v3 | Module actuel | Module cible | Action Rust | +|-----------------------------------------------------------------------------------------------------------------------|------------------------------------------------|------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------| +| `adapter.DecZY86MU5Gj7kppfUCEmd4LbXXuyZH1yHaP2NTqdiZB.saber_decimal_wrapper.V0_0_0.from_solscan.json` | `adapter/saber_decimal_wrapper` | `adapter/saber_decimal_wrapper` | Aucune | +| `adapter.TwRapQCDhWkZRrDaHfZGuHxkZ91gHDRkyuzNqeU5MgR.spl_token_wrap.V0_1_0.from_github_solana_program.json` | `—` | `adapter/spl_token_wrap` | Créer `adapter/spl_token_wrap` | +| `amm.2wT8Yq49kHgDzXuPxZSaeLaH1qbmGXtEyPy64bL7aD3c.lifinity_swap_v2.V0_1_1.from_solscan.json` | `amm/lifinity_swap_v2` | `amm/lifinity_swap_v2` | Aucune | +| `amm.AMMJdEiCCa8mdugg6JPF7gFirmmxisTfDJoSNSUi5zDJ.metadao_v0_5.V0_5_0.from_solscan.json` | `amm/metadao_v0_5` | `amm/metadao_v0_5` | Aucune | +| `amm.BSwp6bEBihVLdqJRKGgzjcGLHkcTuzmSo1TQkHepzH8p.bonk_swap.V0_1_1.from_solscan.json` | `amm/bonk_swap` | `amm/bonk_swap` | Aucune | +| `amm.Eo7WjKq67rjJQSZxS6z3YkapzY3eMj6Xy8X5EQVn5UaB.meteora_damm_v1.V0_5_2.from_solscan.json` | `amm/meteora_damm_v1` | `amm/meteora_damm_v1` | Aucune | +| `amm.GAMMA7meSFWaBXF25oSUgmGRwaW6sCMFLmBNiMSdbHVT.goosefx_gamma.V0_2_0.from_solscan.json` | `amm/goosefx_gamma` | `amm/goosefx_gamma` | Aucune | +| `amm.GFXsSL5sSaDfNFQUYsHekbWBW1TsFdjDYzACh62tEHxn.goosefx_v2.V0_3_0.from_solscan.json` | `amm/goosefx_v2` | `amm/goosefx_v2` | Aucune | +| `amm.Gswppe6ERWKpUTXvRPfXdzHhiCyJvLadVvXGfdpBqcE1.guac_swap.V0_1_1.from_solscan.json` | `amm/guac_swap` | `amm/guac_swap` | Aucune | +| `amm.WooFif76YGRNjk1pA8wCsN67aQsD9f9iLsz4NcJ1AVb.woofi.V0_1_0.from_solscan.json` | `amm/woofi` | `amm/woofi` | Aucune | +| `amm.cpamdpZCGKUy5JxQXB4dcpGPiikHawvSWAd6mEn1sGG.meteora_damm_v2.V0_2_0.from_solscan.json` | `amm/meteora_damm_v2` | `amm/meteora_damm_v2` | Aucune | +| `amm.pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA.pump_swap.V0_1_0.from_solscan.json` | `amm/pump_swap` | `amm/pump_swap` | Aucune | +| `amm.vrTGoBuy5rYSxAfV3jaRJWHH6nN9WK4NRExGxsk1bCJ.vertigo.V0_1_0.from_solscan.json` | `amm/vertigo` | `amm/vertigo` | Aucune | +| `bridge.6doghB248px58JSSwG4qejQ46kFMW4AMj7vzJnWZHNZn.layer_zero_executor.V0_1_0.from_solscan.json` | `bridge/layer_zero_executor` | `bridge/layer_zero_executor` | Aucune | +| `bridge.76y77prsiCMvXMjuoZ5VRrhG5qYBrUMYTE5WgHqgjEn6.layer_zero_endpoint.V0_1_0.from_solscan.json` | `bridge/layer_zero_endpoint` | `bridge/layer_zero_endpoint` | Aucune | +| `bridge.CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe.circle_cctp_token_messenger_minter_v2.V0_2_0.from_solscan.json` | `bridge/circle_cctp_token_messenger_minter_v2` | `bridge/circle_cctp_token_messenger_minter_v2` | Aucune | +| `bridge.CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3.circle_cctp_token_messenger_minter.V0_1_0.from_solscan.json` | `bridge/circle_cctp_token_messenger_minter` | `bridge/circle_cctp_token_messenger_minter` | Aucune | +| `clmm.6dMXqGZ3ga2dikrYS9ovDXgHGh5RUsb2RTUj6hrQXhk6.stabble.V0_1_0.from_solscan.json` | `clmm/stabble` | `clmm/stabble` | Aucune | +| `clmm.CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK.raydium.V0_1_0.from_solscan.json` | `clmm/raydium` | `clmm/raydium` | Aucune | +| `clmm.HpNfyc2Saw7RKkQd8nEL4khUcuPhQ7WwY1B2qjx8jxFq.pancake_swap.V0_1_0.from_solscan.json` | `clmm/pancake_swap` | `clmm/pancake_swap` | Aucune | +| `clmm.REALQqNEomY6cQGZJUGwywTBD2UmDT32rZcNnfxQ5N2.byreal.V0_1_0.from_solscan.json` | `clmm/byreal` | `clmm/byreal` | Aucune | +| `clmm.fUSioN9YKKSa3CUC2YUc4tPkHJ5Y6XW1yz8y6F7qWz9.fusion.V1_1_4.from_solscan.json` | `clmm/fusion` | `clmm/fusion` | Aucune | +| `clmm.whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc.orca_whirlpool.V0_9_0.from_solscan.json` | `clmm/orca_whirlpool` | `clmm/orca_whirlpool` | Aucune | +| `cpmm.CPMMoo8L3F4NbTegBCKVNunggL7H1ZpdTHKxQB5qKP1C.raydium.V0_2_0.from_solscan.json` | `cpmm/raydium` | `cpmm/raydium` | Aucune | +| `dlmm.LBUZKhRxPF3XUpBCjp4YzTKgLccjZhTSDM9YuVaPwxo.meteora.V0_12_0.from_solscan.json` | `dlmm/meteora` | `dlmm/meteora` | Aucune | +| `fees.pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ.pump_fees.V0_1_0.from_solscan.json` | `admin/pump_fees` | `fees/pump_fees` | Déplacer `admin/pump_fees` → `fees/pump_fees` | +| `governance.FUTARELBfJfQ8RDGhg1wdhddq1odMAJUePHFuBYfUxKq.metadao_futarchy.V0_6_1.from_solscan.json` | `amm/metadao_futarchy_amm` | `governance/metadao_futarchy` | Déplacer `amm/metadao_futarchy_amm` → `governance/metadao_futarchy` | +| `governance.SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf.squads_multisig.V2_0_0.from_solscan.json` | `—` | `governance/squads_multisig` | Créer `governance/squads_multisig` | +| `governance.WALL8ucBuUyL46QYxwYJjidaFYhdvxUFrgvBxPshERx.metadao_bid_wall.V0_7_0.from_solscan.json` | `governance/metadao_bid_wall` | `governance/metadao_bid_wall` | Aucune | +| `launchpad.5U3EU2ubXtK84QcRjWVmYt9RaDyA8gKxdUrPFXmZyaki.virtuals.V0_1_0.from_solscan.json` | `amm/virtuals` | `launchpad/virtuals` | Déplacer `amm/virtuals` → `launchpad/virtuals` | +| `launchpad.6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P.pump_fun.V0_1_0.from_solscan.json` | `launchpad/pump_fun` | `launchpad/pump_fun` | Aucune | +| `launchpad.LanMV9sAd7wArD4vJFi2qDdfnVhFxYSUg6eADduJ3uj.raydium_launchlab.V0_2_0.from_solscan.json` | `launchpad/raydium_launchlab` | `launchpad/raydium_launchlab` | Aucune | +| `launchpad.MoonCVVNZFSYkqNXP6bxHLPL6QQJiMagDL3qcqUQTrG.moonit.V0_1_0.from_solscan.json` | `launchpad/moonit` | `launchpad/moonit` | Aucune | +| `launchpad.PdMDrKEMaX8q7CCJb7NvUCxerBCcsFUa4LjBEynTtEd.pump_pumpup_ai.V0_1_0.from_solscan.json` | `launchpad/pump_pumpup_ai` | `launchpad/pump_pumpup_ai` | Aucune | +| `launchpad.T8HsGYv7sMk3kTnyaRqZrbRPuntYzdh12evXBkprint.printr.V1_0_0.from_solscan.json` | `launchpad/printr` | `launchpad/printr` | Aucune | +| `launchpad.boop8hVGQGqehUK2iVEMEnMrL5RbjywRzHKBmBE7ry4.boop_fun.V0_3_0.from_solscan.json` | `launchpad/boop_fun` | `launchpad/boop_fun` | Aucune | +| `launchpad.dbcij3LWUppWqq96dh6gJWwBifmcGfLSB5D4DuSMaqN.meteora_dbc.V0_1_10.from_solscan.json` | `launchpad/meteora_dbc` | `launchpad/meteora_dbc` | Aucune | +| `launchpad.moontUzsdepotRGe5xsfip7vLPTJnVuafqdUWexVnPM.metadao_ico.V0_7_0.from_solscan.json` | `launchpad/metadao_ico` | `launchpad/metadao_ico` | Aucune | +| `launchpad.waveQX2yP3H1pVU8djGvEHmYg8uamQ84AuyGtpsrXTF.orca_wavebreak.V1_1_5.from_solscan.json` | `launchpad/orca_wavebreak` | `launchpad/orca_wavebreak` | Aucune | +| `lending.C1onEW2kPetmHmwe74YC1ESx3LnFEpVau6g2pg4fHycr.clone.V0_1_0.from_solscan.json` | `lending/clone` | `lending/clone` | Aucune | +| `lending.KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD.kamino.V1_23_0.from_solscan.json` | `lending/kamino` | `lending/kamino` | Aucune | +| `lending.MFv2hWf31Z9kbCa1snEPYctwafyhdvnV7FZnsebVacA.marginfi_v2.V0_1_8.from_solscan.json` | `lending/marginfi_v2` | `lending/marginfi_v2` | Aucune | +| `lock.LockrWmn6K5twhz3y9w1dQERbmgSaRkfnTeTKbpofwE.raydium_lp.V0_1_0.from_solscan.json` | `lock/raydium_lp` | `lock/raydium_lp` | Aucune | +| `metadata.ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S.solana_program_metadata.V0_0_0.from_github_solana_program.json` | `metadata/solana_program_metadata` | `metadata/solana_program_metadata` | Modèles wire, décodeurs de comptes et neuf instructions stables présents ; implémenter la matérialisation et l’exécuteur | +| `metadata.metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s.metaplex_token_metadata.V1_14_0.from_solscan.json` | `metadata/metaplex_token_metadata` | `metadata/metaplex_token_metadata` | Aucune | +| `nft.BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY.metaplex_bubblegum.V0_12_0.from_solscan.json` | `nft/metaplex_bubblegum` | `nft/metaplex_bubblegum` | Aucune | +| `nft.TCMPhJdwDryooaGtiocG1u3xcYbRpiJzb283XfCZsDp.tensor_cnft.V2_0_1.from_solscan.json` | `—` | `nft/tensor_cnft` | Créer `nft/tensor_cnft` | +| `orderbook.j1o2qRpjcyUwEvwtcfhEQefh773ZgjxcVRry7LDqg5X.jupiter_limit_order_v2.V0_1_0.from_solscan.json` | `orderbook/jupiter_limit_order_v2` | `orderbook/jupiter_limit_order_v2` | Aucune | +| `orderbook.jupoNjAxXgZ4rjzxzPMP4oxduvQsQtZzyknqvzYNrNu.jupiter_limit_order.V0_1_0.from_solscan.json` | `orderbook/jupiter_limit_order` | `orderbook/jupiter_limit_order` | Aucune | +| `orderbook.opnb2LAfJYbRMAHHvqjCwQxanZn7ReEHp1k81EohpZb.openbook_v2.V0_1_0.from_solscan.json` | `orderbook/openbook_v2` | `orderbook/openbook_v2` | Aucune | +| `perpetuals.EtrnLzgbS7nMMy5fbD42kXiUzGg8XQzJ972Xtk1cjWih.phoenix_eternal.V0_2_0.from_solscan.json` | `—` | `perpetuals/phoenix_eternal` | Créer `perpetuals/phoenix_eternal` | +| `perpetuals.PERPHjGBqRHArX4DySjwM6UJHiR3sWAatqfdBS2qQJu.jupiter.V0_1_0.from_solscan.json` | `perpetuals/jupiter` | `perpetuals/jupiter` | Aucune | +| `perpetuals.ZETAxsqBRek56DhiGXrn75yj2NHU3aYUnxvHXpkf3aD.zeta.V0_1_0.from_solscan.json` | `perpetuals/zeta` | `perpetuals/zeta` | Aucune | +| `perpetuals.dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH.drift_v2.V2_150_0.from_solscan.json` | `perpetuals/drift_v2` | `perpetuals/drift_v2` | Aucune | +| `router.6m2CDdhRgxpH4WjvdzxAYbGxwdGUz5MziiL5jek2kBma.okx_labs_v1.V0_1_0.from_solscan.json` | `router/okx_labs_v1` | `router/okx_labs_v1` | Aucune | +| `router.DF1ow4tspfHX9JwWJsAb9epbkA8hmpSEAtxXy1V27QBH.dflow_aggregator_v4.V0_1_0.from_solscan.json` | `router/dflow_aggregator_v4` | `router/dflow_aggregator_v4` | Aucune | +| `router.JUP4Fb2cqiRUcaTHdrPC8h2gNsA2ETXiPDD33WcGuJB.jupiter_aggregator_v4.V0_1_0.from_solscan.json` | `router/jupiter_aggregator_v4` | `router/jupiter_aggregator_v4` | Aucune | +| `router.JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4.jupiter_aggregator_v6.V0_1_0.from_solscan.json` | `router/jupiter_aggregator_v6` | `router/jupiter_aggregator_v6` | Aucune | +| `router.proVF4pMXVaYqmy4NjniPh4pqKNfMmsihgd4wdkCX3u.okx_labs_v2.V0_1_0.from_solscan.json` | `router/okx_labs_v2` | `router/okx_labs_v2` | Aucune | +| `rwa.XzTT4XB8m7sLD2xi6snefSasaswsKCxx5Tifjondogm.ondo_global_markets.V0_1_0.from_solscan.json` | `rwa/ondo_global_markets` | `rwa/ondo_global_markets` | Aucune | +| `solana.11111111111111111111111111111111.core.system.V0_0_1.from_github_solana_program.json` | `solana/core/system` | `solana/core/system` | Aucune | +| `solana.AddressLookupTab1e1111111111111111111111111.core.address_lookup_table.V0_0_1.from_github_solana_program.json` | `solana/core/address_lookup_table` | `solana/core/address_lookup_table` | Aucune | +| `solana.BPFLoaderUpgradeab1e11111111111111111111111.core.loader_v3.V0_0_0.from_github_solana_program.json` | `solana/core/loader_v3` | `solana/core/loader_v3` | Aucune | +| `solana.ComputeBudget111111111111111111111111111111.core.compute_budget.V0_0_1.from_github_solana_program.json` | `solana/core/compute_budget` | `solana/core/compute_budget` | Aucune | +| `solana.Config1111111111111111111111111111111111111.core.config.V0_0_1.from_github_solana_program.json` | `solana/core/config` | `solana/core/config` | Aucune | +| `solana.Feature111111111111111111111111111111111111.core.feature.V0_0_1.from_github_solana_program.json` | `solana/core/feature` | `solana/core/feature` | Aucune | +| `solana.LoaderV411111111111111111111111111111111111.core.loader_v4.V0_0_0.from_github_solana_program.json` | `solana/core/loader_v4` | `solana/core/loader_v4` | Aucune | +| `solana.Stake11111111111111111111111111111111111111.core.stake.V4_3_1.from_github_solana_program.json` | `solana/core/stake` | `solana/core/stake` | Aucune | +| `spl.MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr.memo.v3.V3_0_1.from_github_solana_program.json` | `spl/memo/v3` | `spl/memo/v3` | Aucune | +| `spl.TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.token.V3_3_0.from_github_solana_program.json` | `spl/token` | `spl/token` | Aucune | +| `spl.TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.token_2022.V3_0_1.from_github_solana_program.json` | `spl/token_2022` | `spl/token_2022` | Aucune | +| `stable.HYEXCHtHkBagdStcJCp3xbbb9B7sdMdWXFNj6mdsG4hn.swap_hylo_exchange.V0_1_4.from_solscan.json` | `stable/swap_hylo_exchange` | `stable/swap_hylo_exchange` | Aucune | +| `stable.JUPUSDecMzAVgztLe6eGhwUBj1Pn3j9WAXwmtHmfbRr.swap_jupiter_stable.V0_1_0.from_solscan.json` | `stable/swap_jupiter_stable` | `stable/swap_jupiter_stable` | Aucune | +| `stable.NUMERUNsFCP3kuNmWZuXtm1AaQCPj9uw6Guv2Ekoi5P.swap_numeraire.V0_1_0.from_solscan.json` | `stable/swap_numeraire` | `stable/swap_numeraire` | Aucune | +| `stable.swapNyd8XiQwJ6ianp9snpu4brUqFxadzvHebnAXjJZ.swap_stabble.V1_5_0.from_solscan.json` | `stable/swap_stabble` | `stable/swap_stabble` | Aucune | +| `staking.4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7.jito_tip_distribution.V0_1_5.from_solscan.json` | `—` | `staking/jito_tip_distribution` | Créer `staking/jito_tip_distribution` | +| `staking.FarmsPZpWu9i7Kky8tPN37rs2TpmMrAZrC7S7vJa91Hr.kamino_farm.V1_6_5.from_solscan.json` | `staking/kamino_farm` | `staking/kamino_farm` | Aucune | +| `staking.MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD.marinade_finance.V0_1_0.from_solscan.json` | `staking/marinade_finance` | `staking/marinade_finance` | Aucune | +| `storage.recr1L3PCGKLbckBqMNcJhuuyU1zgo8nBhfLVsJNwr5.solana_record.V0_4_0.from_github_solana_program.json` | `—` | `storage/solana_record` | Créer `storage/solana_record` | +| `strategy.DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M.jupiter_dca.V0_1_0.from_solscan.json` | `strategy/jupiter_dca` | `strategy/jupiter_dca` | Déplacer `strategy/jupiter_dca` → `strategy/jupiter_dca` | +| `treasury.treaf4wWBBty3fHdyBpo35Mz84M8k3heKXmjmi9vFt5.helium_treasury_management.V0_2_3.from_solscan.json` | `treasury/helium_treasury_management` | `treasury/helium_treasury_management` | Aucune | +| `vault.24Uqj9JCLxUeoC3hGfh5W3s9FM9uCHDS2SG3LYwBpyTi.meteora.V0_7_2.from_solscan.json` | `vault/meteora` | `vault/meteora` | Aucune | +| `vault.6LtLpnUFNByNXLyCoK9wA2MykKAmQNZKBdY8s47dehDc.kamino_yvaults.V0_1_0.from_solscan.json` | `vault/kamino_yvaults` | `vault/kamino_yvaults` | Aucune | +| `vault.CarrotwivhMpDnm27EHmRLeQ683Z1PufuqEmBZvD282s.carrot_defi.V0_1_0.from_solscan.json` | `vault/carrot_defi` | `vault/carrot_defi` | Aucune | +| `vault.HysTabVUfmQBFcmzu1ctRd1Y1fxd66RBpboy1bmtDSQQ.hylo_stability_pool.V0_1_4.from_solscan.json` | `vault/hylo_stability_pool` | `vault/hylo_stability_pool` | Aucune | +| `vault.KvauGMspG5k6rtzrqqn7WNn3oZdyKqLKwK2XWQ8FLjd.kamino_v2.V2_1_1.from_solscan.json` | `vault/kamino_v2` | `vault/kamino_v2` | Aucune | +| `vault.kvauTFR8qm1dhniz6pYuBZkuene3Hfrs1VQhVRgCNrr.kamino.V0_1_0.from_solscan.json` | `vault/kamino` | `vault/kamino` | Aucune | +| `vesting.LocpQgucEQHbqNABEYvBvwoxCPsSbG91A1QaQhQQqjn.jupiter_lock.V0_4_0.from_solscan.json` | `admin/jupiter_lock` | `vesting/jupiter_lock` | Déplacer `admin/jupiter_lock` → `vesting/jupiter_lock` | +| `vesting.strmRqUCoQUgGUan5YhzUZa6KqdzwX5L6FpUxfmKg5m.streamflow.V0_1_0.from_solscan.json` | `vesting/streamflow` | `vesting/streamflow` | Aucune | +| `wallet.JSW99DKmxNyREQM14SQLDykeBvEUG63TeohrvmofEiw.jupiter_apepro_smart_wallet.V0_1_0.from_solscan.json` | `wallet/jupiter_apepro_smart_wallet` | `wallet/jupiter_apepro_smart_wallet` | Aucune | +| `weighted.swapFpHZwjELNnjvThjajtiVmkz3yPQEHjLtka2fwHW.swap_stabble.V1_5_0.from_solscan.json` | `weighted/swap_stabble` | `weighted/swap_stabble` | Aucune | diff --git a/docs/README.md b/docs/README.md index 6d515bd..01e3dce 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,5 @@ - + # Documentation active de Khadhroony Bot3 @@ -94,6 +94,11 @@ Les onze crates possèdent désormais `README.md`, `TODO.md`, `USAGE.md` et `CHA - [`validation/WEBSOCKET_MAINNET_RESEARCH_VALIDATION_REPORT.md`](validation/WEBSOCKET_MAINNET_RESEARCH_VALIDATION_REPORT.md) ; - [`validation/MAINNET_RESEARCH_BACKFILL_VALIDATION_SCENARIO.md`](validation/MAINNET_RESEARCH_BACKFILL_VALIDATION_SCENARIO.md) ; +## Audits actifs + +- [`Audit contractuel 0.4.8-pre.002 — metadata`](audits/V0_4_8_PRE_002_METADATA_CONTRACT_AUDIT.md) ; +- [`Audit 0.4.8-pre.005 — instructions et historique Solana Program Metadata`](audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md). + ## Plans de version actifs - [`Plan 0.4.8 — Solana Program Metadata et complétude Token-2022`](plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md). diff --git a/docs/audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md b/docs/audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md new file mode 100644 index 0000000..5b12c51 --- /dev/null +++ b/docs/audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md @@ -0,0 +1,152 @@ + + + +# Audit `0.4.8-pre.005` — instructions et historique de Solana Program Metadata + +## 1. Objet + +Cet audit détermine la surface exacte du décodeur d’instructions du programme : + +```text +ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S +``` + +Il vérifie séparément : + +- l’inventaire stable actuel ; +- les écarts entre l’IDL Codama, les clients générés et le processeur on-chain ; +- l’existence éventuelle d’instructions supprimées, remplacées ou obsolètes ; +- la politique de couverture à appliquer dans `kb-lib`. + +L’audit ne porte ni sur Metaplex Token Metadata, ni sur les metadata incorporées à Token-2022, ni sur la résolution off-chain des URL. + +## 2. Sources de vérité + +Les sources officielles retenues sont : + +- les tags `program@v1.0.0` et `program@v1.0.1` du dépôt `solana-program/program-metadata` ; +- `program/src/instruction.rs` et l’entrypoint du programme ; +- les neuf processeurs sous `program/src/processor/` au tag `program@v1.0.1` ; +- l’IDL Codama officielle conservée brute sous `idls/` ; +- les clients Codama générés, utilisés pour distinguer le contrat SDK canonique des formes supplémentaires acceptées par le runtime ; +- l’historique Git antérieur à la première release stable, utilisé uniquement comme provenance. + +Le processeur `program@v1.0.1` est la source de vérité pour les formes réellement acceptées par le runtime actuel. L’IDL reste la source de vérité du contrat SDK publié. + +## 3. Inventaire stable + +Les deux releases stables auditées exposent le même inventaire de neuf instructions : + +| Tag | Code Khadhroony | Instruction officielle | Statut | +|----:|-----------------|------------------------|--------| +| `0` | `write` | `Write` | stable | +| `1` | `initialize` | `Initialize` | stable | +| `2` | `set_authority` | `SetAuthority` | stable | +| `3` | `set_data` | `SetData` | stable | +| `4` | `set_immutable` | `SetImmutable` | stable | +| `5` | `trim` | `Trim` | stable | +| `6` | `close` | `Close` | stable | +| `7` | `allocate` | `Allocate` | stable | +| `8` | `extend` | `Extend` | stable | + +Aucun discriminant stable supplémentaire n’a été trouvé. Aucun de ces neuf discriminants n’est marqué obsolète dans les releases stables auditées. + +## 4. Historique antérieur à la première release stable + +### 4.1 Tag `5` + +Un commit de travail du 10 janvier 2025 a introduit une instruction nommée `WithdrawExcessLamports` au tag `5`. Le 19 février 2025, avant `program@v1.0.0`, ce même tag a été remplacé par `Trim`. + +Cette évolution ne crée pas une dixième instruction à couvrir : + +- le discriminant reste `5` ; +- le contrat stable publié est `Trim` ; +- aucune release stable auditée n’expose `WithdrawExcessLamports` comme entrée distincte ; +- le comportement de `Trim` comprend désormais le redimensionnement du compte et le retrait de l’excédent de lamports. + +Décision : le décodeur publie uniquement `trim` pour le tag `5`. Le nom pré-stable est conservé dans la provenance de l’observation et dans la matrice, sans entrée de couverture historique distincte. + +### 4.2 Autres formes de travail + +L’historique antérieur à `program@v1.0.0` contient plusieurs changements de wire, de comptes optionnels et de builders. Ils appartiennent à une phase explicitement marquée WIP dans le dépôt. + +Aucune compatibilité réseau historique n’est déclarée pour ces formes tant qu’une preuve de déploiement et une transaction de cluster ne sont pas identifiées. Le décodeur ne doit pas inventer des entrées historiques uniquement à partir de commits de développement. + +## 5. Écarts runtime / IDL à conserver + +### 5.1 `Write` + +Le runtime exige un offset `u32` puis sélectionne exactement une source : + +- octets restants dans l’instruction ; +- ou compte `source_buffer` lorsque le reliquat est vide. + +Les deux sources simultanées et l’absence des deux sources échouent. + +### 5.2 `Initialize` + +Le header d’arguments fixe mesure 20 octets. Le reliquat peut être vide uniquement lorsque le compte metadata est déjà un `Buffer` préalloué ; sinon des données inline sont requises. + +Le décodeur peut établir l’intention wire, mais la validité de cette condition dépend de l’état du compte et sera vérifiée par les lectures stateful futures. + +### 5.3 `SetAuthority` + +Le SDK encode une option canonique `0` ou `1`. Le runtime traite néanmoins tout flag non nul comme une nouvelle autorité et exige alors exactement 32 octets. + +Pour le flag `0`, les octets suffixes sont ignorés sur le chemin `Metadata`. La suppression d’autorité reste interdite pour un `Buffer` et cette distinction nécessite l’état du compte. + +### 5.4 `SetData` + +L’IDL encode `encoding`, `compression`, `format`, `data_source` puis un reliquat optionnel. Le processeur accepte aussi une forme de trois octets contenant uniquement les trois premiers champs ; cette forme modifie le header sans remplacer les données existantes. + +Lorsque `data_source` est présent : + +- un reliquat non vide fournit les données inline et impose le placeholder pour `buffer` ; +- un reliquat vide impose un véritable compte `buffer` ; +- les deux sources ou aucune source sont rejetées. + +### 5.5 Instructions sans arguments SDK + +`SetImmutable`, `Trim` et `Close` ne reçoivent pas leur reliquat depuis l’entrypoint. Le runtime ignore donc les octets suivant leur tag, même si le client SDK canonique n’en produit aucun. + +Le décodeur conserve leur longueur et leur préfixe comme suffixe ignoré ; il ne les interprète pas comme de nouveaux arguments. + +### 5.6 Comptes supplémentaires + +Le runtime accepte des comptes restants pour `Write`, `Initialize`, `SetData`, `Allocate` et `Extend`. Il exige une longueur exacte pour `SetAuthority`, `SetImmutable`, `Trim` et `Close`. + +La matrice contractuelle encode explicitement cette différence. + +## 6. Décision de décodage + +Le décodeur `DcMetadataSolanaProgramMetadataDecoder` : + +- reconnaît uniquement le Program ID officiel ; +- déclare exactement neuf entrées de couverture non historiques ; +- borne la taille de l’instruction et le nombre de comptes contextualisés ; +- valide les discriminateurs, longueurs, enums, flags, ordre des comptes et contrats de sources ; +- conserve les variantes runtime actuelles qui dépassent le contrat SDK ; +- conserve les suffixes runtime ignorés sans leur attribuer de sémantique ; +- produit une intention non committée pour une transaction échouée ; +- ne télécharge aucune URL et ne lit aucun compte externe implicitement ; +- ne déclare aucune forme pré-stable comme compatible réseau sans preuve de cluster. + +## 7. Artefacts liés + +- `test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json` ; +- `kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs` ; +- `kb-lib/tests/external_solana_program_metadata_instruction_api.rs` ; +- `docs/IDL_AUDIT.md` ; +- `docs/IDL_TO_KB_LIB_NOMENCLATURE.md`. + +## 8. Statut de validation + +| Niveau | Statut | +|-------------------------------------------------|---------------------------------------------| +| audit de l’IDL | validé | +| audit des sources `program@v1.0.1` | validé | +| comparaison `program@v1.0.0` / `program@v1.0.1` | validée, inventaire inchangé | +| audit historique pré-stable | documenté, sans revendication réseau | +| tests synthétiques Rust | à exécuter dans le workspace utilisateur | +| replay de transactions réelles | planifié dans les phases pipeline et Devnet | +| validation Devnet | non encore exécutée | diff --git a/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md b/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md index c0f502e..e6e5055 100644 --- a/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md +++ b/docs/plans/V0_4_8_SOLANA_PROGRAM_METADATA_AND_TOKEN_2022_COMPLETENESS_PLAN.md @@ -1,11 +1,11 @@ - + # Plan `0.4.8` — Solana Program Metadata et complétude Token-2022 ## 1. Statut et rôle -Ce document est le livrable principal de `0.4.8-pre.001`, réconcilié après l’audit contractuel de `0.4.8-pre.002`, les modèles de `pre.003` et les décodeurs de comptes de `pre.004`. Il constitue le plan vivant de la version et reste modifiable lorsque l’audit du code, des interfaces officielles, de l’IDL ou des validations Devnet impose un ajustement. +Ce document est le livrable principal de `0.4.8-pre.001`, réconcilié après l’audit contractuel de `0.4.8-pre.002`, les modèles de `pre.003`, les décodeurs de comptes de `pre.004` et le décodeur d’instructions de `pre.005`. Il constitue le plan vivant de la version et reste modifiable lorsque l’audit du code, des interfaces officielles, de l’IDL ou des validations Devnet impose un ajustement. Il doit être maintenu pendant chaque prerelease, puis archivé pendant la dernière prerelease après transfert des décisions durables vers le ROADMAP, les matrices, les guides, les rapports, les TODO et les changelogs concernés. @@ -15,13 +15,13 @@ Il doit être maintenu pendant chaque prerelease, puis archivé pendant la derni ### 2.1 Surfaces strictement distinctes -| Surface | Program ID ou frontière | Rôle dans `0.4.8` | -|---|---|---| -| Metaplex Token Metadata | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` | surface clôturée en `0.4.7`, hors développement principal | -| Token-2022 Token Metadata | programme Token-2022 `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` | compléter le contrat déjà partiellement pris en charge | -| `spl-token-metadata-interface` | interface sans Program ID autonome imposé | source contractuelle des instructions metadata implémentées par Token-2022 | -| Solana Program Metadata | `ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S` | nouvelle surface on-chain à implémenter sous `metadata/solana_program_metadata` | -| contenu distant HTTP/IPFS/Arweave | transport off-chain indépendant | hors `0.4.8`, reporté à l’horizon `0.15+` | +| Surface | Program ID ou frontière | Rôle dans `0.4.8` | +|-----------------------------------|--------------------------------------------------------------------|---------------------------------------------------------------------------------| +| Metaplex Token Metadata | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` | surface clôturée en `0.4.7`, hors développement principal | +| Token-2022 Token Metadata | programme Token-2022 `TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb` | compléter le contrat déjà partiellement pris en charge | +| `spl-token-metadata-interface` | interface sans Program ID autonome imposé | source contractuelle des instructions metadata implémentées par Token-2022 | +| Solana Program Metadata | `ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S` | nouvelle surface on-chain à implémenter sous `metadata/solana_program_metadata` | +| contenu distant HTTP/IPFS/Arweave | transport off-chain indépendant | hors `0.4.8`, reporté à l’horizon `0.15+` | Aucun décodeur Token-2022 existant n’est un décodeur partiel de `ProgM6…`. Les deux contrats doivent conserver des namespaces, modèles, matrices, tests et scénarios distincts. @@ -208,7 +208,7 @@ Validation définitive reçue le 5 août 2026 : - `python3 scripts/audit_rust_workspace_rules.py` : propre ; - `cargo test -p kb-lib` : 647 tests unitaires, cinq tests d’intégration et les doc-tests réussis. -### `0.4.8-pre.004` — décodeur de comptes Solana Program Metadata — terminé côté delta +### `0.4.8-pre.004` — décodeur de comptes Solana Program Metadata — terminé Résultat durable : `kb-lib` expose des snapshots et parseurs bornés pour `Buffer` et `Metadata`. Le propriétaire, les discriminateurs, les headers, les enums fermées, les longueurs logiques, les sources de données et les PDA metadata sont vérifiés. Les données après `data_length` restent une capacité allouée valide ; les données Buffer occupent tout le reliquat. Aucune URL ni référence externe n’est résolue. @@ -219,12 +219,20 @@ Résultat durable : `kb-lib` expose des snapshots et parseurs bornés pour `Buff - nomenclature IDL réconciliée avec le statut réel des comptes ; - test de façade publique pour les consommateurs externes. -Critère atteint côté delta : comptes exacts, erreurs fail-closed et politique de trailing bytes documentée ; compilation, Clippy et tests Rust restent à exécuter dans l’environnement du projet avant validation définitive. +Validation définitive reçue le 5 août 2026 : `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --all-targets` et l’audit workspace sont propres ; `cargo test -p kb-lib` réussit avec 653 tests unitaires, six tests d’intégration et les doc-tests. -### `0.4.8-pre.005` — décodeur d’instructions Solana Program Metadata +### `0.4.8-pre.005` — décodeur d’instructions Solana Program Metadata — terminé côté delta -- neuf instructions confirmées ; -- comptes, signataires, arguments, échecs et instructions internes/externes. +Résultat durable : les releases stables `program@v1.0.0` et `program@v1.0.1` exposent exactement neuf discriminants `0..8`. Le tag `5`, nommé `WithdrawExcessLamports` pendant une phase pré-stable, est couvert uniquement comme `Trim` et conserve cet ancien nom comme provenance. Les variantes runtime actuelles qui dépassent l’IDL sont explicitement préservées. + +- neuf instructions stables confirmées et activées ; +- comptes, signataires, flags writable, arguments, échecs et instructions internes/externes ; +- sélection exclusive des sources inline ou Buffer ; +- distinction entre longueurs de comptes exactes et comptes restants acceptés par le runtime ; +- matrice contractuelle et audit historique dédiés ; +- tests synthétiques et test de façade publique. + +Critère atteint côté delta : inventaire stable et historique fermé, décodeur fail-closed et frontières off-chain conservées ; compilation, Clippy et tests Rust restent à exécuter dans l’environnement du projet avant validation définitive. ### `0.4.8-pre.006` — matérialisation Solana Program Metadata diff --git a/kb-lib/CHANGELOG.md b/kb-lib/CHANGELOG.md index 5b7cb11..3716943 100644 --- a/kb-lib/CHANGELOG.md +++ b/kb-lib/CHANGELOG.md @@ -1,8 +1,20 @@ - + # CHANGELOG — kb-lib +## 0.4.8-pre.005 — instructions Solana Program Metadata + +- active `DcMetadataSolanaProgramMetadataDecoder` pour les neuf discriminants stables `0..8` ; +- décode les arguments, comptes positionnels, signers, flags writable et sources inline ou Buffer avec des bornes explicites ; +- conserve les formes runtime actuelles non produites par le SDK, notamment `SetData` sur trois octets et les suffixes ignorés de `SetImmutable`, `Trim` et `Close` ; +- distingue comptes restants acceptés par le runtime et longueurs de comptes exactes ; +- documente le nom pré-stable `WithdrawExcessLamports` du tag `5` sans créer une entrée historique distincte de `Trim` ; +- ajoute la matrice d’instructions, l’audit historique, les tests positifs/négatifs et le test de façade publique ; +- retire le décodeur Solana Program Metadata de l’inventaire des décodeurs réservés. +- aligne la structure du composant sur les règles du workspace : façade limitée aux sous-modules et réexports, implémentation déplacée dans `decoder.rs`, constantes centralisées dans `constants.rs` et accès internes via `crate::`. +- corrige les vecteurs négatifs `Initialize` afin de placer explicitement les valeurs invalides après les 16 octets du seed et de vérifier séparément `Encoding`, `Compression`, `Format` et `DataSource`. + ## 0.4.8-pre.004 — comptes Solana Program Metadata - ajoute les décodeurs publics bornés des comptes `Buffer` et `Metadata` ainsi que leur sélection par discriminateur ; diff --git a/kb-lib/README.md b/kb-lib/README.md index 43d0a70..70f9386 100644 --- a/kb-lib/README.md +++ b/kb-lib/README.md @@ -1,5 +1,5 @@ - + # kb-lib @@ -7,7 +7,7 @@ ## Périmètre -La crate couvre actuellement Solana Core, SPL Memo, SPL Token classique, SPL Associated Token Account, Token-2022, le registre SPL ElGamal et Metaplex Token Metadata. Elle expose également les modèles wire bornés et les décodeurs de comptes `Buffer` et `Metadata` de Solana Program Metadata ; le décodeur d’instructions reste réservé jusqu’à la phase suivante. +La crate couvre actuellement Solana Core, SPL Memo, SPL Token classique, SPL Associated Token Account, Token-2022, le registre SPL ElGamal et Metaplex Token Metadata. Elle expose également les modèles wire bornés, les décodeurs de comptes `Buffer` et `Metadata` et le décodeur contextualisé des neuf instructions stables de Solana Program Metadata. Pour Solana Program Metadata, la surface publique fondatrice comprend : @@ -17,7 +17,10 @@ Pour Solana Program Metadata, la surface publique fondatrice comprend : - les données directes, URL et références vers un autre compte sans résolution off-chain ; - les cinq erreurs custom officielles et les erreurs de conversion fail-closed ; - les snapshots bornés `Buffer` et `Metadata`, avec validation du propriétaire, des PDA metadata, des longueurs logiques et des trois sources de données ; -- une politique explicite où les octets situés après `data_length` sont conservés comme capacité allouée et jamais assimilés au contenu logique. +- une politique explicite où les octets situés après `data_length` sont conservés comme capacité allouée et jamais assimilés au contenu logique ; +- les neuf instructions stables `Write`, `Initialize`, `SetAuthority`, `SetData`, `SetImmutable`, `Trim`, `Close`, `Allocate` et `Extend` ; +- les formes runtime actuelles non produites par le SDK, notamment `SetData` sans remplacement de données et les suffixes ignorés de `SetImmutable`, `Trim` et `Close` ; +- une frontière historique où l’ancien nom pré-stable `WithdrawExcessLamports` du tag `5` reste une provenance de `Trim`, sans dixième entrée de couverture. Pour Metaplex Token Metadata, elle fournit : @@ -50,7 +53,7 @@ Pour Metaplex Token Metadata, elle fournit : - contrats `MtApi*` et matérialisateurs concrets ; - contrats `ExApi*`, politiques de sécurité et exécuteurs `Ex*Executor` ; - `ExMetadataMetaplexTokenMetadataExecutor`, `ExMetaplexTokenMetadataExecutionIntent` et `ExMetaplexTokenMetadataOperation` ; -- modèles `DcMetadataSpm*`, constantes `DC_METADATA_SPM_*` et fonctions `decoder_metadata_solana_program_metadata_decode_*_account` pour le contrat Solana Program Metadata ; +- `DcMetadataSolanaProgramMetadataDecoder`, modèles `DcMetadataSpm*`, constantes `DC_METADATA_SPM_*` et fonctions `decoder_metadata_solana_program_metadata_decode_*_account` pour le contrat Solana Program Metadata ; - modèles canoniques `Md*` réexportés par la façade. Les exemples d’appel et invariants sont documentés dans [USAGE.md](USAGE.md). @@ -65,7 +68,7 @@ Les exemples d’appel et invariants sont documentés dans [USAGE.md](USAGE.md). La surface fonctionnelle Metaplex de la crate est achevée. Les campagnes Devnet restantes relèvent de `kb-pipeline-demo-scenarios` et de `kb-app-demo-desktop`, non d’un manque de builder dans `kb-lib`. -Pour Solana Program Metadata, les modèles wire et les décodeurs bornés de comptes sont actifs. `Buffer` accepte tout reliquat comme données allouées sans inventer de PDA stable après changement d’autorité ; `Metadata` valide la dérivation canonical ou non-canonical et distingue le contenu logique de la capacité ajoutée par `Extend`. Le décodeur de protocole reste volontairement réservé jusqu’à l’implémentation des neuf instructions. +Pour Solana Program Metadata, les modèles wire, les décodeurs bornés de comptes et le décodeur contextualisé des neuf instructions stables sont actifs. `Buffer` accepte tout reliquat comme données allouées sans inventer de PDA stable après changement d’autorité ; `Metadata` valide la dérivation canonical ou non-canonical et distingue le contenu logique de la capacité ajoutée par `Extend`. Les variantes qui dépendent de l’état du compte restent des intentions wire jusqu’aux lectures stateful prévues, et aucune forme pré-stable n’est déclarée compatible réseau sans preuve de cluster. Le registre ElGamal n’est pas déclaré validé réellement sur réseau. @@ -76,3 +79,5 @@ Le registre ElGamal n’est pas déclaré validé réellement sur réseau. - [Historique des changements](CHANGELOG.md) - [Matrice contractuelle Metaplex](../test-fixtures/contract-matrices/METAPLEX_TOKEN_METADATA_MATRIX.json) - [Matrice des comptes Solana Program Metadata](../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_ACCOUNT_MATRIX.json) +- [Matrice des instructions Solana Program Metadata](../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json) +- [Audit de l’inventaire et de l’historique des instructions](../docs/audits/V0_4_8_PRE_005_SOLANA_PROGRAM_METADATA_INSTRUCTION_HISTORY_AUDIT.md) diff --git a/kb-lib/USAGE.md b/kb-lib/USAGE.md index 734bf4c..b0ddda1 100644 --- a/kb-lib/USAGE.md +++ b/kb-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de kb-lib @@ -51,7 +51,8 @@ Décodeurs fonctionnels principaux : - `DcSplAssociatedTokenAccountDecoder` ; - `DcSplToken2022Decoder` ; - `DcSplElgamalRegistryDecoder` ; -- `DcMetadataMetaplexTokenMetadataDecoder`. +- `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. @@ -82,7 +83,7 @@ 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 et comptes Solana Program Metadata +## 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`. @@ -138,7 +139,19 @@ match snapshot { 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 de protocole `DcMetadataSolanaProgramMetadataDecoder` reste réservé aux instructions. Les comptes constituent désormais un contrat public stable pour les futurs matérialisateurs, builders et parcours stateful. +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. ## Comptes Metaplex Token Metadata diff --git a/kb-lib/src/decoder.rs b/kb-lib/src/decoder.rs index 48586ad..5e67faf 100644 --- a/kb-lib/src/decoder.rs +++ b/kb-lib/src/decoder.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/decoder.rs -// version: 14 +// version: 16 //! Consolidated decoder modules. @@ -36,7 +36,6 @@ mod weighted; /// Reserved `kb-lib.decoder.adapter.saber_decimal_wrapper` decoder. pub use self::adapter::DcAdapterSaberDecimalWrapperDecoder; -/// Approve Collection Authority discriminator. /// Reserved `kb-lib.decoder.adapter.spl_token_wrap` decoder. pub use self::adapter::DcAdapterSplTokenWrapDecoder; /// Reserved `kb-lib.decoder.amm.aldrin_v1` decoder. @@ -201,6 +200,7 @@ pub use self::lending::DcLendingKaminoDecoder; pub use self::lending::DcLendingMarginfiV2Decoder; /// Reserved `kb-lib.decoder.lock.raydium_lp` decoder. pub use self::lock::DcLockRaydiumLpDecoder; +/// Approve Collection Authority discriminator. pub(crate) use self::metadata::DC_METADATA_MTM_APPROVE_COLLECTION_AUTHORITY_DISCRIMINATOR; /// Approve Use Authority discriminator. pub(crate) use self::metadata::DC_METADATA_MTM_APPROVE_USE_AUTHORITY_DISCRIMINATOR; @@ -332,16 +332,50 @@ pub(crate) use self::metadata::DC_METADATA_MTM_VERIFY_COLLECTION_DISCRIMINATOR; pub(crate) use self::metadata::DC_METADATA_MTM_VERIFY_DISCRIMINATOR; /// Verify Sized Collection Item discriminator. pub(crate) use self::metadata::DC_METADATA_MTM_VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR; +/// Account contract for `Allocate`. +pub(crate) use self::metadata::DC_METADATA_SPM_ALLOCATE_ACCOUNT_CONTRACTS; /// Exact fixed byte length of one Buffer account header. pub use self::metadata::DC_METADATA_SPM_BUFFER_HEADER_BYTES; +/// Account contract for `Close`. +pub(crate) use self::metadata::DC_METADATA_SPM_CLOSE_ACCOUNT_CONTRACTS; +/// Surfaces handled by the Solana Program Metadata decoder. +pub(crate) use self::metadata::DC_METADATA_SPM_DECODER_SURFACES; +/// Maximum instruction prefix retained in hexadecimal diagnostics. +pub(crate) use self::metadata::DC_METADATA_SPM_DIAGNOSTIC_PREFIX_BYTES; +/// Stable event schema version for decoded Solana Program Metadata instructions. +pub(crate) use self::metadata::DC_METADATA_SPM_EVENT_VERSION; +/// Account contract for `Extend`. +pub(crate) use self::metadata::DC_METADATA_SPM_EXTEND_ACCOUNT_CONTRACTS; /// Exact byte length of one external-account data reference. pub use self::metadata::DC_METADATA_SPM_EXTERNAL_DATA_BYTES; +/// Account contract for `Initialize`. +pub(crate) use self::metadata::DC_METADATA_SPM_INITIALIZE_ACCOUNT_CONTRACTS; +/// Current stable instruction inventory for program releases 1.0.0 and 1.0.1. +pub(crate) use self::metadata::DC_METADATA_SPM_INSTRUCTION_ENTRIES; /// Maximum Solana account-data length accepted by bounded account decoders. pub use self::metadata::DC_METADATA_SPM_MAX_ACCOUNT_BYTES; +/// Maximum instruction-account count accepted by the contextual decoder. +pub(crate) use self::metadata::DC_METADATA_SPM_MAX_INSTRUCTION_ACCOUNTS; +/// Maximum retained instruction payload accepted by the contextual decoder. +pub(crate) use self::metadata::DC_METADATA_SPM_MAX_INSTRUCTION_BYTES; /// Exact fixed byte length of one Metadata account header. pub use self::metadata::DC_METADATA_SPM_METADATA_HEADER_BYTES; +/// Program ids handled by the Solana Program Metadata decoder. +pub(crate) use self::metadata::DC_METADATA_SPM_PROGRAM_IDS; /// Exact byte length of one Solana Program Metadata seed. pub use self::metadata::DC_METADATA_SPM_SEED_BYTES; +/// Account contract for `SetAuthority`. +pub(crate) use self::metadata::DC_METADATA_SPM_SET_AUTHORITY_ACCOUNT_CONTRACTS; +/// Account contract for `SetData`. +pub(crate) use self::metadata::DC_METADATA_SPM_SET_DATA_ACCOUNT_CONTRACTS; +/// Account contract for `SetImmutable`. +pub(crate) use self::metadata::DC_METADATA_SPM_SET_IMMUTABLE_ACCOUNT_CONTRACTS; +/// Stable Solana Program Metadata decoder surface code. +pub(crate) use self::metadata::DC_METADATA_SPM_SURFACE_CODE; +/// Account contract for `Trim`. +pub(crate) use self::metadata::DC_METADATA_SPM_TRIM_ACCOUNT_CONTRACTS; +/// Account contract for `Write`. +pub(crate) use self::metadata::DC_METADATA_SPM_WRITE_ACCOUNT_CONTRACTS; /// Exact decoder for the Metaplex Token Metadata program. pub use self::metadata::DcMetadataMetaplexTokenMetadataDecoder; /// Canonical projection shared by Collection and Use Authority records. @@ -414,6 +448,8 @@ pub use self::metadata::DcMetadataSpmEncoding; pub use self::metadata::DcMetadataSpmExternalData; /// Format declared for Solana Program Metadata content. pub use self::metadata::DcMetadataSpmFormat; +/// Expected account role and privileges for one instruction position. +pub(crate) use self::metadata::DcMetadataSpmInstructionAccountContract; /// Bounded projection of one initialized Solana Program Metadata account. pub use self::metadata::DcMetadataSpmMetadataAccountSnapshot; /// Bounded Solana Program Metadata model conversion error. @@ -424,6 +460,8 @@ pub use self::metadata::DcMetadataSpmProgramError; pub use self::metadata::DcMetadataSpmSeed; /// Canonical tracing target for this crate. pub(crate) use self::metadata::TRACING_TARGET_DECODER_METADATA_METAPLEX_TOKEN_METADATA; +/// Canonical tracing target for the Solana Program Metadata decoder. +pub(crate) use self::metadata::TRACING_TARGET_DECODER_METADATA_SOLANA_PROGRAM_METADATA; /// Crate-root access to `decode` from `instruction`. pub(crate) use self::metadata::decoder_metadata_metaplex_token_metadata_decode; /// Decodes one Collection Authority Record account. @@ -450,12 +488,18 @@ pub use self::metadata::decoder_metadata_metaplex_token_metadata_decode_use_auth pub(crate) use self::metadata::decoder_metadata_metaplex_token_metadata_entry_for_discriminator; /// Crate-root access to `payload_discriminator` from `instruction`. pub(crate) use self::metadata::decoder_metadata_metaplex_token_metadata_payload_discriminator; +/// Decodes one bounded Solana Program Metadata instruction. +pub(crate) use self::metadata::decoder_metadata_solana_program_metadata_decode; /// Decodes one supported Solana Program Metadata account by its discriminator. pub use self::metadata::decoder_metadata_solana_program_metadata_decode_account; /// Decodes one Solana Program Metadata Buffer account. pub use self::metadata::decoder_metadata_solana_program_metadata_decode_buffer_account; /// Decodes one initialized Solana Program Metadata account. pub use self::metadata::decoder_metadata_solana_program_metadata_decode_metadata_account; +/// Returns the stable entry matching one Solana Program Metadata discriminator. +pub(crate) use self::metadata::decoder_metadata_solana_program_metadata_entry_for_discriminator; +/// Returns the first byte of one retained Solana Program Metadata payload. +pub(crate) use self::metadata::decoder_metadata_solana_program_metadata_payload_discriminator; /// Reserved `kb-lib.decoder.nft.metaplex_bubblegum` decoder. pub use self::nft::DcNftMetaplexBubblegumDecoder; /// Reserved `kb-lib.decoder.nft.tensor_cnft` decoder. @@ -755,11 +799,10 @@ pub use self::weighted::DcWeightedSwapStabbleDecoder; mod reserved_decoder_tests { #[test] fn every_reserved_decoder_exposes_only_registered_program_ids() { - let decoders: [&dyn crate::DcApiProtocolDecoder; 105] = [ + let decoders: [&dyn crate::DcApiProtocolDecoder; 104] = [ &crate::DcAdapterSaberDecimalWrapperDecoder, &crate::DcAdapterSplTokenWrapDecoder, &crate::DcGovernanceSquadsMultisigDecoder, - &crate::DcMetadataSolanaProgramMetadataDecoder, &crate::DcNftTensorCnftDecoder, &crate::DcPerpetualsPhoenixEternalDecoder, &crate::DcStakingJitoTipDistributionDecoder, @@ -891,6 +934,6 @@ mod reserved_decoder_tests { assert!(decode_result.is_ok_and(|events| return events.is_empty())); } } - assert_eq!(names.len(), 105); + assert_eq!(names.len(), 104); } } diff --git a/kb-lib/src/decoder/metadata.rs b/kb-lib/src/decoder/metadata.rs index 18377e4..c214029 100644 --- a/kb-lib/src/decoder/metadata.rs +++ b/kb-lib/src/decoder/metadata.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/decoder/metadata.rs -// version: 9 +// version: 11 //! `metadata` decoder family. @@ -87,7 +87,7 @@ pub use self::solana_program_metadata::DC_METADATA_SPM_MAX_ACCOUNT_BYTES; pub use self::solana_program_metadata::DC_METADATA_SPM_METADATA_HEADER_BYTES; /// Exact byte length of one Solana Program Metadata seed. pub use self::solana_program_metadata::DC_METADATA_SPM_SEED_BYTES; -/// Solana Program Metadata instruction decoder; account decoders are active. +/// Exact Solana Program Metadata instruction and account decoder. pub use self::solana_program_metadata::DcMetadataSolanaProgramMetadataDecoder; /// Error returned while decoding a Solana Program Metadata account. pub use self::solana_program_metadata::DcMetadataSpmAccountDecodeError; @@ -264,3 +264,47 @@ pub(crate) use self::metaplex_token_metadata::decoder_metadata_metaplex_token_me pub(crate) use self::metaplex_token_metadata::decoder_metadata_metaplex_token_metadata_entry_for_discriminator; /// Crate-root access to `payload_discriminator` from `instruction`. pub(crate) use self::metaplex_token_metadata::decoder_metadata_metaplex_token_metadata_payload_discriminator; +/// Account contract for `Allocate`. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_ALLOCATE_ACCOUNT_CONTRACTS; +/// Account contract for `Close`. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_CLOSE_ACCOUNT_CONTRACTS; +/// Surfaces handled by the Solana Program Metadata decoder. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_DECODER_SURFACES; +/// Maximum instruction prefix retained in hexadecimal diagnostics. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_DIAGNOSTIC_PREFIX_BYTES; +/// Stable event schema version for decoded Solana Program Metadata instructions. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_EVENT_VERSION; +/// Account contract for `Extend`. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_EXTEND_ACCOUNT_CONTRACTS; +/// Account contract for `Initialize`. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_INITIALIZE_ACCOUNT_CONTRACTS; +/// Current stable instruction inventory for program releases 1.0.0 and 1.0.1. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_INSTRUCTION_ENTRIES; +/// Maximum instruction-account count accepted by the contextual decoder. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_MAX_INSTRUCTION_ACCOUNTS; +/// Maximum retained instruction payload accepted by the contextual decoder. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_MAX_INSTRUCTION_BYTES; +/// Program ids handled by the Solana Program Metadata decoder. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_PROGRAM_IDS; +/// Account contract for `SetAuthority`. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_SET_AUTHORITY_ACCOUNT_CONTRACTS; +/// Account contract for `SetData`. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_SET_DATA_ACCOUNT_CONTRACTS; +/// Account contract for `SetImmutable`. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_SET_IMMUTABLE_ACCOUNT_CONTRACTS; +/// Stable Solana Program Metadata decoder surface code. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_SURFACE_CODE; +/// Account contract for `Trim`. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_TRIM_ACCOUNT_CONTRACTS; +/// Account contract for `Write`. +pub(crate) use self::solana_program_metadata::DC_METADATA_SPM_WRITE_ACCOUNT_CONTRACTS; +/// Expected account role and privileges for one instruction position. +pub(crate) use self::solana_program_metadata::DcMetadataSpmInstructionAccountContract; +/// Canonical tracing target for the Solana Program Metadata decoder. +pub(crate) use self::solana_program_metadata::TRACING_TARGET_DECODER_METADATA_SOLANA_PROGRAM_METADATA; +/// Decodes one bounded Solana Program Metadata instruction. +pub(crate) use self::solana_program_metadata::decoder_metadata_solana_program_metadata_decode; +/// Returns the stable entry matching one Solana Program Metadata discriminator. +pub(crate) use self::solana_program_metadata::decoder_metadata_solana_program_metadata_entry_for_discriminator; +/// Returns the first byte of one retained Solana Program Metadata payload. +pub(crate) use self::solana_program_metadata::decoder_metadata_solana_program_metadata_payload_discriminator; diff --git a/kb-lib/src/decoder/metadata/solana_program_metadata.rs b/kb-lib/src/decoder/metadata/solana_program_metadata.rs index 4a98fb8..81e6db3 100644 --- a/kb-lib/src/decoder/metadata/solana_program_metadata.rs +++ b/kb-lib/src/decoder/metadata/solana_program_metadata.rs @@ -1,10 +1,12 @@ // file: kb-lib/src/decoder/metadata/solana_program_metadata.rs -// version: 3 +// version: 5 -//! Public Solana Program Metadata models, account decoders and reserved instruction decoder. +//! Public Solana Program Metadata models and exact account and instruction decoders. mod account; mod constants; +mod decoder; +mod instruction; mod model; /// Error returned while decoding a Solana Program Metadata account. @@ -31,6 +33,8 @@ pub use self::constants::DC_METADATA_SPM_MAX_ACCOUNT_BYTES; pub use self::constants::DC_METADATA_SPM_METADATA_HEADER_BYTES; /// Exact byte length of one Solana Program Metadata seed. pub use self::constants::DC_METADATA_SPM_SEED_BYTES; +/// Exact decoder for Solana Program Metadata instructions and account snapshots. +pub use self::decoder::DcMetadataSolanaProgramMetadataDecoder; /// Solana Program Metadata account discriminator. pub use self::model::DcMetadataSpmAccountDiscriminator; /// Compression declared for metadata content. @@ -52,37 +56,47 @@ pub use self::model::DcMetadataSpmProgramError; /// Exact fixed-size metadata seed. pub use self::model::DcMetadataSpmSeed; -/// Decoder reserved for Solana Program Metadata instructions; account decoding is exposed separately. -#[derive(Clone, Debug, Default)] -pub struct DcMetadataSolanaProgramMetadataDecoder; - -impl crate::DcApiProtocolDecoder for crate::DcMetadataSolanaProgramMetadataDecoder { - fn decoder_name(&self) -> &'static str { - return "kb-lib.decoder.metadata.solana_program_metadata"; - } - - fn decoder_version(&self) -> &'static str { - return env!("CARGO_PKG_VERSION"); - } - - fn program_ids(&self) -> &'static [&'static str] { - return &[kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID]; - } - - fn supports_observation( - &self, - observation: &crate::MdProgramObservation, - ) -> crate::DcApiDecoderSupport { - if crate::DcApiProtocolDecoder::handles_program_id(self, &observation.program_id) { - return crate::DcApiDecoderSupport::Maybe; - } - return crate::DcApiDecoderSupport::No; - } - - fn decode_observation( - &self, - _observation: &crate::MdProgramObservation, - ) -> kb_core::Result> { - return std::result::Result::Ok(std::vec::Vec::new()); - } -} +/// Account contract for `Allocate`. +pub(crate) use self::constants::DC_METADATA_SPM_ALLOCATE_ACCOUNT_CONTRACTS; +/// Account contract for `Close`. +pub(crate) use self::constants::DC_METADATA_SPM_CLOSE_ACCOUNT_CONTRACTS; +/// Surfaces handled by the Solana Program Metadata decoder. +pub(crate) use self::constants::DC_METADATA_SPM_DECODER_SURFACES; +/// Maximum instruction prefix retained in hexadecimal diagnostics. +pub(crate) use self::constants::DC_METADATA_SPM_DIAGNOSTIC_PREFIX_BYTES; +/// Stable event schema version for decoded Solana Program Metadata instructions. +pub(crate) use self::constants::DC_METADATA_SPM_EVENT_VERSION; +/// Account contract for `Extend`. +pub(crate) use self::constants::DC_METADATA_SPM_EXTEND_ACCOUNT_CONTRACTS; +/// Account contract for `Initialize`. +pub(crate) use self::constants::DC_METADATA_SPM_INITIALIZE_ACCOUNT_CONTRACTS; +/// Current stable instruction inventory for program releases 1.0.0 and 1.0.1. +pub(crate) use self::constants::DC_METADATA_SPM_INSTRUCTION_ENTRIES; +/// Maximum instruction-account count accepted by the contextual decoder. +pub(crate) use self::constants::DC_METADATA_SPM_MAX_INSTRUCTION_ACCOUNTS; +/// Maximum retained instruction payload accepted by the contextual decoder. +pub(crate) use self::constants::DC_METADATA_SPM_MAX_INSTRUCTION_BYTES; +/// Program ids handled by the Solana Program Metadata decoder. +pub(crate) use self::constants::DC_METADATA_SPM_PROGRAM_IDS; +/// Account contract for `SetAuthority`. +pub(crate) use self::constants::DC_METADATA_SPM_SET_AUTHORITY_ACCOUNT_CONTRACTS; +/// Account contract for `SetData`. +pub(crate) use self::constants::DC_METADATA_SPM_SET_DATA_ACCOUNT_CONTRACTS; +/// Account contract for `SetImmutable`. +pub(crate) use self::constants::DC_METADATA_SPM_SET_IMMUTABLE_ACCOUNT_CONTRACTS; +/// Stable Solana Program Metadata decoder surface code. +pub(crate) use self::constants::DC_METADATA_SPM_SURFACE_CODE; +/// Account contract for `Trim`. +pub(crate) use self::constants::DC_METADATA_SPM_TRIM_ACCOUNT_CONTRACTS; +/// Account contract for `Write`. +pub(crate) use self::constants::DC_METADATA_SPM_WRITE_ACCOUNT_CONTRACTS; +/// Expected account role and privileges for one instruction position. +pub(crate) use self::constants::DcMetadataSpmInstructionAccountContract; +/// Canonical tracing target for the Solana Program Metadata decoder. +pub(crate) use self::constants::TRACING_TARGET_DECODER_METADATA_SOLANA_PROGRAM_METADATA; +/// Decodes one bounded Solana Program Metadata instruction. +pub(crate) use self::instruction::decoder_metadata_solana_program_metadata_decode; +/// Returns the stable entry matching one Solana Program Metadata discriminator. +pub(crate) use self::instruction::decoder_metadata_solana_program_metadata_entry_for_discriminator; +/// Returns the first byte of one retained Solana Program Metadata payload. +pub(crate) use self::instruction::decoder_metadata_solana_program_metadata_payload_discriminator; diff --git a/kb-lib/src/decoder/metadata/solana_program_metadata/constants.rs b/kb-lib/src/decoder/metadata/solana_program_metadata/constants.rs index 1f51f5f..d20bb7a 100644 --- a/kb-lib/src/decoder/metadata/solana_program_metadata/constants.rs +++ b/kb-lib/src/decoder/metadata/solana_program_metadata/constants.rs @@ -1,7 +1,16 @@ // file: kb-lib/src/decoder/metadata/solana_program_metadata/constants.rs -// version: 2 +// version: 4 -//! Bounded Solana Program Metadata wire constants. +//! Bounded Solana Program Metadata wire and decoder constants. + +/// Expected account role and privileges for one instruction position. +#[derive(Clone, Copy, Debug)] +pub(crate) struct DcMetadataSpmInstructionAccountContract { + pub(crate) role: &'static str, + pub(crate) signer: bool, + pub(crate) writable: bool, + pub(crate) optional: bool, +} /// Exact byte length of one Solana Program Metadata seed. pub const DC_METADATA_SPM_SEED_BYTES: usize = 16; @@ -13,3 +22,320 @@ pub const DC_METADATA_SPM_METADATA_HEADER_BYTES: usize = 96; pub const DC_METADATA_SPM_EXTERNAL_DATA_BYTES: usize = 40; /// Maximum Solana account-data length accepted by bounded account decoders. pub const DC_METADATA_SPM_MAX_ACCOUNT_BYTES: usize = 10_485_760; +/// Maximum retained instruction payload accepted by the contextual decoder. +pub(crate) const DC_METADATA_SPM_MAX_INSTRUCTION_BYTES: usize = 16_384; +/// Maximum instruction-account count accepted by the contextual decoder. +pub(crate) const DC_METADATA_SPM_MAX_INSTRUCTION_ACCOUNTS: usize = 16; +/// Maximum instruction prefix retained in hexadecimal diagnostics. +pub(crate) const DC_METADATA_SPM_DIAGNOSTIC_PREFIX_BYTES: usize = 64; +/// Stable Solana Program Metadata decoder surface code. +pub(crate) const DC_METADATA_SPM_SURFACE_CODE: &str = "metadata.solana_program_metadata"; +/// Stable event schema version for decoded Solana Program Metadata instructions. +pub(crate) const DC_METADATA_SPM_EVENT_VERSION: u16 = 1; +/// Current stable instruction inventory for program releases 1.0.0 and 1.0.1. +pub(crate) const DC_METADATA_SPM_INSTRUCTION_ENTRIES: &[(u8, &str)] = &[ + (0, "write"), + (1, "initialize"), + (2, "set_authority"), + (3, "set_data"), + (4, "set_immutable"), + (5, "trim"), + (6, "close"), + (7, "allocate"), + (8, "extend"), +]; +/// Program ids handled by the Solana Program Metadata decoder. +pub(crate) const DC_METADATA_SPM_PROGRAM_IDS: &[&str] = + &[kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID]; +/// Surfaces handled by the Solana Program Metadata decoder. +pub(crate) const DC_METADATA_SPM_DECODER_SURFACES: &[crate::DcApiDecoderSurface] = + &[crate::DcApiDecoderSurface { + program_id: kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID, + surface_code: crate::DC_METADATA_SPM_SURFACE_CODE, + priority: 100, + }]; +/// Account contract for `Write`. +pub(crate) const DC_METADATA_SPM_WRITE_ACCOUNT_CONTRACTS: + &[crate::DcMetadataSpmInstructionAccountContract] = &[ + crate::DcMetadataSpmInstructionAccountContract { + role: "buffer", + signer: false, + writable: true, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "authority", + signer: true, + writable: false, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "source_buffer", + signer: false, + writable: false, + optional: true, + }, +]; +/// Account contract for `Initialize`. +pub(crate) const DC_METADATA_SPM_INITIALIZE_ACCOUNT_CONTRACTS: + &[crate::DcMetadataSpmInstructionAccountContract] = &[ + crate::DcMetadataSpmInstructionAccountContract { + role: "metadata", + signer: false, + writable: true, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "authority", + signer: true, + writable: false, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program", + signer: false, + writable: false, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program_data", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "system_program", + signer: false, + writable: false, + optional: true, + }, +]; +/// Account contract for `SetAuthority`. +pub(crate) const DC_METADATA_SPM_SET_AUTHORITY_ACCOUNT_CONTRACTS: + &[crate::DcMetadataSpmInstructionAccountContract] = &[ + crate::DcMetadataSpmInstructionAccountContract { + role: "account", + signer: false, + writable: true, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "authority", + signer: true, + writable: false, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program_data", + signer: false, + writable: false, + optional: true, + }, +]; +/// Account contract for `SetData`. +pub(crate) const DC_METADATA_SPM_SET_DATA_ACCOUNT_CONTRACTS: + &[crate::DcMetadataSpmInstructionAccountContract] = &[ + crate::DcMetadataSpmInstructionAccountContract { + role: "metadata", + signer: false, + writable: true, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "authority", + signer: true, + writable: false, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "buffer", + signer: false, + writable: true, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program_data", + signer: false, + writable: false, + optional: true, + }, +]; +/// Account contract for `SetImmutable`. +pub(crate) const DC_METADATA_SPM_SET_IMMUTABLE_ACCOUNT_CONTRACTS: + &[crate::DcMetadataSpmInstructionAccountContract] = &[ + crate::DcMetadataSpmInstructionAccountContract { + role: "metadata", + signer: false, + writable: true, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "authority", + signer: true, + writable: false, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program_data", + signer: false, + writable: false, + optional: true, + }, +]; +/// Account contract for `Trim`. +pub(crate) const DC_METADATA_SPM_TRIM_ACCOUNT_CONTRACTS: + &[crate::DcMetadataSpmInstructionAccountContract] = &[ + crate::DcMetadataSpmInstructionAccountContract { + role: "account", + signer: false, + writable: true, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "authority", + signer: true, + writable: false, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program_data", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "destination", + signer: false, + writable: true, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "rent_sysvar", + signer: false, + writable: false, + optional: false, + }, +]; +/// Account contract for `Close`. +pub(crate) const DC_METADATA_SPM_CLOSE_ACCOUNT_CONTRACTS: + &[crate::DcMetadataSpmInstructionAccountContract] = &[ + crate::DcMetadataSpmInstructionAccountContract { + role: "account", + signer: false, + writable: true, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "authority", + signer: true, + writable: false, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program_data", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "destination", + signer: false, + writable: true, + optional: false, + }, +]; +/// Account contract for `Allocate`. +pub(crate) const DC_METADATA_SPM_ALLOCATE_ACCOUNT_CONTRACTS: + &[crate::DcMetadataSpmInstructionAccountContract] = &[ + crate::DcMetadataSpmInstructionAccountContract { + role: "buffer", + signer: false, + writable: true, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "authority", + signer: true, + writable: false, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program_data", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "system_program", + signer: false, + writable: false, + optional: true, + }, +]; +/// Account contract for `Extend`. +pub(crate) const DC_METADATA_SPM_EXTEND_ACCOUNT_CONTRACTS: + &[crate::DcMetadataSpmInstructionAccountContract] = &[ + crate::DcMetadataSpmInstructionAccountContract { + role: "account", + signer: false, + writable: true, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "authority", + signer: true, + writable: false, + optional: false, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program", + signer: false, + writable: false, + optional: true, + }, + crate::DcMetadataSpmInstructionAccountContract { + role: "program_data", + signer: false, + writable: false, + optional: true, + }, +]; +/// Canonical tracing target for the Solana Program Metadata decoder. +pub(crate) const TRACING_TARGET_DECODER_METADATA_SOLANA_PROGRAM_METADATA: &str = + "kb-lib.decoder.metadata.solana_program_metadata"; diff --git a/kb-lib/src/decoder/metadata/solana_program_metadata/decoder.rs b/kb-lib/src/decoder/metadata/solana_program_metadata/decoder.rs new file mode 100644 index 0000000..d8b7134 --- /dev/null +++ b/kb-lib/src/decoder/metadata/solana_program_metadata/decoder.rs @@ -0,0 +1,134 @@ +// file: kb-lib/src/decoder/metadata/solana_program_metadata/decoder.rs +// version: 1 + +//! Exact contextual dispatch for Solana Program Metadata. + +/// Exact decoder for Solana Program Metadata instructions and account snapshots. +#[derive(Clone, Debug, Default)] +pub struct DcMetadataSolanaProgramMetadataDecoder; + +impl crate::DcApiProtocolDecoder for crate::DcMetadataSolanaProgramMetadataDecoder { + fn decoder_name(&self) -> &'static str { + return "kb-lib.decoder.metadata.solana_program_metadata"; + } + + fn decoder_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn program_ids(&self) -> &'static [&'static str] { + return crate::DC_METADATA_SPM_PROGRAM_IDS; + } + + fn supports_observation( + &self, + observation: &crate::MdProgramObservation, + ) -> crate::DcApiDecoderSupport { + return if crate::DcApiProtocolDecoder::handles_program_id(self, &observation.program_id) { + crate::DcApiDecoderSupport::Yes + } else { + crate::DcApiDecoderSupport::No + }; + } + + fn decode_observation( + &self, + _observation: &crate::MdProgramObservation, + ) -> kb_core::Result> { + return std::result::Result::Ok(std::vec::Vec::new()); + } +} + +impl crate::DcApiInstructionDecoder for crate::DcMetadataSolanaProgramMetadataDecoder { + fn identity(&self) -> crate::DcApiDecoderIdentity { + return crate::DcApiDecoderIdentity { + name: crate::DC_METADATA_SPM_SURFACE_CODE.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }; + } + + fn surfaces(&self) -> &'static [crate::DcApiDecoderSurface] { + return crate::DC_METADATA_SPM_DECODER_SURFACES; + } + + fn coverage(&self) -> std::vec::Vec { + return crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES + .iter() + .map(|(tag, code)| { + return crate::DcApiDecoderCoverageDeclaration { + program_id: kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID + .to_string(), + surface_code: std::option::Option::Some( + crate::DC_METADATA_SPM_SURFACE_CODE.to_string(), + ), + entry_kind: crate::DcApiDecoderCoverageEntryKind::Instruction, + entry_code: (*code).to_string(), + discriminator_hex: std::option::Option::Some(format!("{tag:02x}")), + historical: false, + }; + }) + .collect(); + } + + fn recognize( + &self, + input: &crate::MdCoreInstructionReplayInput, + ) -> crate::DcApiDecoderRecognition { + if input.program_id != kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID { + return crate::DcApiDecoderRecognition::incompatible(); + } + let discriminator = + crate::decoder_metadata_solana_program_metadata_payload_discriminator(input); + let entry = discriminator + .and_then(crate::decoder_metadata_solana_program_metadata_entry_for_discriminator); + return crate::DcApiDecoderRecognition::compatible( + entry.is_some(), + 100, + std::option::Option::Some(crate::DC_METADATA_SPM_SURFACE_CODE.to_string()), + entry.map(|(_, code)| return code.to_string()), + discriminator.map(|value| return format!("{value:02x}")), + ); + } + + fn decode( + &self, + input: &crate::MdCoreInstructionReplayInput, + ) -> crate::DcApiDecoderExecutionResult { + if input.program_id != kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID { + return crate::DcApiDecoderExecutionResult::unsupported(std::option::Option::None); + } + let result = crate::decoder_metadata_solana_program_metadata_decode(input); + if matches!( + result.status, + crate::DcApiDecoderOutcomeStatus::Failed + | crate::DcApiDecoderOutcomeStatus::Unsupported + ) { + tracing::error!( + target: crate::TRACING_TARGET_DECODER_METADATA_SOLANA_PROGRAM_METADATA, + action = "decode_failure", + signature = %input.signature, + slot = input.slot, + instruction_path = %input.instruction_path, + program_id = %input.program_id, + processor_name = crate::DC_METADATA_SPM_SURFACE_CODE, + processor_version = env!("CARGO_PKG_VERSION"), + input_key = %input.replay_input_key, + transaction_failed = input.transaction_failed, + result_status = ?result.status, + diagnostics = ?result.diagnostics, + "Solana Program Metadata instruction was not decoded successfully" + ); + } + tracing::debug!( + target: crate::TRACING_TARGET_DECODER_METADATA_SOLANA_PROGRAM_METADATA, + action = "decode", + signature = %input.signature, + instruction_path = %input.instruction_path, + transaction_failed = input.transaction_failed, + result_status = ?result.status, + observation_count = result.observations.len(), + "Solana Program Metadata instruction decode completed" + ); + return result; + } +} diff --git a/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs b/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs new file mode 100644 index 0000000..ce251eb --- /dev/null +++ b/kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs @@ -0,0 +1,1282 @@ +// file: kb-lib/src/decoder/metadata/solana_program_metadata/instruction.rs +// version: 3 + +//! Exact contextual instruction decoding for Solana Program Metadata. + +use base64::Engine; // rust-rules: trait-import +use sha2::Digest; // rust-rules: trait-import + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum SourceContract { + None, + Write { inline_data_length: usize }, + SetDataPreserve, + SetDataBuffer, + SetDataInline { inline_data_length: usize }, + Allocate { seed_present: bool }, +} + +#[derive(Clone, Debug)] +struct ParsedInstruction { + code: &'static str, + tag: u8, + parameters: serde_json::Value, + source_contract: SourceContract, + runtime_ignored_suffix_length: 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 decoder_metadata_solana_program_metadata_entry_for_discriminator( + discriminator: u8, +) -> std::option::Option<(u8, &'static str)> { + return crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES + .iter() + .copied() + .find(|(candidate, _)| return *candidate == discriminator); +} + +pub(crate) fn decoder_metadata_solana_program_metadata_payload_discriminator( + input: &crate::MdCoreInstructionReplayInput, +) -> std::option::Option { + let encoded = input + .instruction_payload_json + .as_ref() + .and_then(|payload| return payload.get("dataBase64")) + .and_then(serde_json::Value::as_str); + let encoded = match encoded { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::option::Option::None, + }; + let decoded = base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()); + return decoded.ok().and_then(|bytes| return bytes.first().copied()); +} + +pub(crate) fn decoder_metadata_solana_program_metadata_decode( + input: &crate::MdCoreInstructionReplayInput, +) -> crate::DcApiDecoderExecutionResult { + let bytes = match decode_payload(input) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => { + return failed( + std::option::Option::None, + "solana_program_metadata_payload_invalid", + message, + ); + }, + }; + let entry = bytes.first().and_then(|tag| { + return crate::decoder_metadata_solana_program_metadata_entry_for_discriminator(*tag) + .map(|(_, code)| return code); + }); + let parsed = match parse_instruction(bytes.as_slice()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => { + return failed(entry, "solana_program_metadata_wire_invalid", message); + }, + }; + let accounts = match resolve_accounts(input) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => { + return failed( + std::option::Option::Some(parsed.code), + "solana_program_metadata_accounts_invalid", + message, + ); + }, + }; + if let std::result::Result::Err(message) = validate_accounts(&parsed, accounts.as_slice()) { + return failed( + std::option::Option::Some(parsed.code), + "solana_program_metadata_accounts_invalid", + message, + ); + } + let account_json = accounts_json(parsed.code, accounts.as_slice()); + let observation = observation(input, &parsed, account_json, bytes.as_slice()); + return crate::DcApiDecoderExecutionResult { + status: crate::DcApiDecoderOutcomeStatus::Decoded, + recognized_entry_code: std::option::Option::Some(parsed.code.to_string()), + observations: std::vec![observation], + diagnostics: std::vec::Vec::new(), + }; +} + +fn decode_payload( + input: &crate::MdCoreInstructionReplayInput, +) -> std::result::Result, std::string::String> { + let payload = match input.instruction_payload_json.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err("instruction payload is not retained".to_string()); + }, + }; + 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( + "instruction payload does not contain dataBase64".to_string(), + ); + }, + }; + let maximum_encoded = crate::DC_METADATA_SPM_MAX_INSTRUCTION_BYTES + .saturating_add(2) + .saturating_div(3) + .saturating_mul(4); + if encoded.len() > maximum_encoded { + return std::result::Result::Err( + "base64 instruction payload exceeds the bounded encoded length".to_string(), + ); + } + let bytes = 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(format!( + "instruction payload is not valid base64: {error}" + )); + }, + }; + if bytes.is_empty() { + return std::result::Result::Err("instruction payload is empty".to_string()); + } + if bytes.len() > crate::DC_METADATA_SPM_MAX_INSTRUCTION_BYTES { + return std::result::Result::Err(format!( + "instruction payload exceeds {} bytes", + crate::DC_METADATA_SPM_MAX_INSTRUCTION_BYTES + )); + } + return std::result::Result::Ok(bytes); +} + +fn parse_instruction(bytes: &[u8]) -> std::result::Result { + let tag = bytes[0]; + let body = &bytes[1..]; + return match tag { + 0 => parse_write(tag, body), + 1 => parse_initialize(tag, body), + 2 => parse_set_authority(tag, body), + 3 => parse_set_data(tag, body), + 4 => parse_runtime_ignored_suffix(tag, "set_immutable", body), + 5 => parse_runtime_ignored_suffix(tag, "trim", body), + 6 => parse_runtime_ignored_suffix(tag, "close", body), + 7 => parse_allocate(tag, body), + 8 => parse_extend(tag, body), + _ => std::result::Result::Err(format!( + "unknown Solana Program Metadata instruction tag {tag}" + )), + }; +} + +fn parse_write( + tag: u8, + body: &[u8], +) -> std::result::Result { + if body.len() < 4 { + return std::result::Result::Err(format!( + "write requires at least 4 argument bytes; received {}", + body.len() + )); + } + let offset = u32::from_le_bytes([body[0], body[1], body[2], body[3]]); + let data = &body[4..]; + return std::result::Result::Ok(ParsedInstruction { + code: "write", + tag, + parameters: serde_json::json!({ + "offset": offset, + "data": data_summary(data), + "dataProvidedInline": !data.is_empty(), + }), + source_contract: SourceContract::Write { inline_data_length: data.len() }, + runtime_ignored_suffix_length: 0, + }); +} + +fn parse_initialize( + tag: u8, + body: &[u8], +) -> std::result::Result { + if body.len() < 20 { + return std::result::Result::Err(format!( + "initialize requires at least 20 argument bytes; received {}", + body.len() + )); + } + let seed = match crate::DcMetadataSpmSeed::from_wire_bytes(&body[..16]) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err( + "initialize contains an invalid seed length".to_string(), + ); + }, + }; + let encoding = match parse_encoding(body[16]) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + let compression = match parse_compression(body[17]) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + let format = match parse_format(body[18]) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + let data_source = match parse_data_source(body[19]) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + let data = &body[20..]; + if !data.is_empty() { + if let std::result::Result::Err(message) = validate_data_length(data_source, data.len()) { + return std::result::Result::Err(message); + } + } + return std::result::Result::Ok(ParsedInstruction { + code: "initialize", + tag, + parameters: serde_json::json!({ + "seed": seed, + "encoding": encoding, + "compression": compression, + "format": format, + "dataSource": data_source, + "data": data_summary(data), + "dataProvidedInline": !data.is_empty(), + "emptyInlineDataRequiresPreallocatedBufferState": data.is_empty(), + }), + source_contract: SourceContract::None, + runtime_ignored_suffix_length: 0, + }); +} + +fn parse_set_authority( + tag: u8, + body: &[u8], +) -> std::result::Result { + if body.is_empty() { + return std::result::Result::Err("set_authority requires an option flag".to_string()); + } + let option_flag = body[0]; + let authority_bytes = &body[1..]; + if option_flag == 0 { + return std::result::Result::Ok(ParsedInstruction { + code: "set_authority", + tag, + parameters: serde_json::json!({ + "optionFlag": option_flag, + "newAuthority": serde_json::Value::Null, + "removeAuthority": true, + "runtimeIgnoredSuffix": byte_summary(authority_bytes), + "bufferRemovalAllowed": false, + }), + source_contract: SourceContract::None, + runtime_ignored_suffix_length: authority_bytes.len(), + }); + } + if authority_bytes.len() != 32 { + return std::result::Result::Err(format!( + "set_authority with a nonzero option flag requires 32 authority bytes; received {}", + authority_bytes.len() + )); + } + let mut public_key_bytes = [0_u8; 32]; + public_key_bytes.copy_from_slice(authority_bytes); + let new_authority = solana_pubkey::Pubkey::new_from_array(public_key_bytes); + return std::result::Result::Ok(ParsedInstruction { + code: "set_authority", + tag, + parameters: serde_json::json!({ + "optionFlag": option_flag, + "canonicalOptionFlag": option_flag == 1, + "newAuthority": new_authority.to_string(), + "newAuthorityIsZero": public_key_bytes.iter().all(|byte| return *byte == 0), + "removeAuthority": false, + }), + source_contract: SourceContract::None, + runtime_ignored_suffix_length: 0, + }); +} + +fn parse_set_data( + tag: u8, + body: &[u8], +) -> std::result::Result { + if body.len() < 3 { + return std::result::Result::Err(format!( + "set_data requires at least 3 argument bytes; received {}", + body.len() + )); + } + let encoding = match parse_encoding(body[0]) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + let compression = match parse_compression(body[1]) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + let format = match parse_format(body[2]) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + if body.len() == 3 { + return std::result::Result::Ok(ParsedInstruction { + code: "set_data", + tag, + parameters: serde_json::json!({ + "encoding": encoding, + "compression": compression, + "format": format, + "dataSource": serde_json::Value::Null, + "data": serde_json::Value::Null, + "dataMutation": "preserve_existing_data", + "processorOnlyVariant": true, + }), + source_contract: SourceContract::SetDataPreserve, + runtime_ignored_suffix_length: 0, + }); + } + let data_source = match parse_data_source(body[3]) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => return std::result::Result::Err(message), + }; + let data = &body[4..]; + if data.is_empty() { + return std::result::Result::Ok(ParsedInstruction { + code: "set_data", + tag, + parameters: serde_json::json!({ + "encoding": encoding, + "compression": compression, + "format": format, + "dataSource": data_source, + "data": data_summary(data), + "dataMutation": "copy_from_buffer", + "processorOnlyVariant": false, + }), + source_contract: SourceContract::SetDataBuffer, + runtime_ignored_suffix_length: 0, + }); + } + if let std::result::Result::Err(message) = validate_data_length(data_source, data.len()) { + return std::result::Result::Err(message); + } + return std::result::Result::Ok(ParsedInstruction { + code: "set_data", + tag, + parameters: serde_json::json!({ + "encoding": encoding, + "compression": compression, + "format": format, + "dataSource": data_source, + "data": data_summary(data), + "dataMutation": "replace_with_inline_data", + "processorOnlyVariant": false, + }), + source_contract: SourceContract::SetDataInline { inline_data_length: data.len() }, + runtime_ignored_suffix_length: 0, + }); +} + +fn parse_runtime_ignored_suffix( + tag: u8, + code: &'static str, + body: &[u8], +) -> std::result::Result { + return std::result::Result::Ok(ParsedInstruction { + code, + tag, + parameters: serde_json::json!({ + "runtimeIgnoredSuffix": byte_summary(body), + "canonicalSdkPayload": body.is_empty(), + }), + source_contract: SourceContract::None, + runtime_ignored_suffix_length: body.len(), + }); +} + +fn parse_allocate( + tag: u8, + body: &[u8], +) -> std::result::Result { + if !body.is_empty() && body.len() != crate::DC_METADATA_SPM_SEED_BYTES { + return std::result::Result::Err(format!( + "allocate requires zero or {} seed bytes; received {}", + crate::DC_METADATA_SPM_SEED_BYTES, + body.len() + )); + } + let seed = if body.is_empty() { + std::option::Option::None + } else { + match crate::DcMetadataSpmSeed::from_wire_bytes(body) { + std::result::Result::Ok(value) => std::option::Option::Some(value), + std::result::Result::Err(_) => { + return std::result::Result::Err( + "allocate contains an invalid seed length".to_string(), + ); + }, + } + }; + return std::result::Result::Ok(ParsedInstruction { + code: "allocate", + tag, + parameters: serde_json::json!({ + "seed": seed, + "bufferKind": if seed.is_some() { "pda" } else { "keypair" }, + }), + source_contract: SourceContract::Allocate { seed_present: seed.is_some() }, + runtime_ignored_suffix_length: 0, + }); +} + +fn parse_extend( + tag: u8, + body: &[u8], +) -> std::result::Result { + if body.len() != 2 { + return std::result::Result::Err(format!( + "extend requires exactly 2 argument bytes; received {}", + body.len() + )); + } + let length = u16::from_le_bytes([body[0], body[1]]); + return std::result::Result::Ok(ParsedInstruction { + code: "extend", + tag, + parameters: serde_json::json!({"length": length}), + source_contract: SourceContract::None, + runtime_ignored_suffix_length: 0, + }); +} + +fn parse_encoding( + value: u8, +) -> std::result::Result { + return crate::DcMetadataSpmEncoding::from_wire_value(value) + .map_err(|_| return format!("unknown encoding value {value}")); +} + +fn parse_compression( + value: u8, +) -> std::result::Result { + return crate::DcMetadataSpmCompression::from_wire_value(value) + .map_err(|_| return format!("unknown compression value {value}")); +} + +fn parse_format(value: u8) -> std::result::Result { + return crate::DcMetadataSpmFormat::from_wire_value(value) + .map_err(|_| return format!("unknown format value {value}")); +} + +fn parse_data_source( + value: u8, +) -> std::result::Result { + return crate::DcMetadataSpmDataSource::from_wire_value(value) + .map_err(|_| return format!("unknown data source value {value}")); +} + +fn validate_data_length( + data_source: crate::DcMetadataSpmDataSource, + length: usize, +) -> std::result::Result<(), std::string::String> { + let valid = match data_source { + crate::DcMetadataSpmDataSource::Direct | crate::DcMetadataSpmDataSource::Url => length > 0, + crate::DcMetadataSpmDataSource::External => { + length == crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES + }, + }; + if !valid { + return std::result::Result::Err(format!( + "data source {:?} does not accept a data length of {length}", + data_source + )); + } + return std::result::Result::Ok(()); +} + +fn resolve_accounts( + input: &crate::MdCoreInstructionReplayInput, +) -> std::result::Result, std::string::String> { + 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( + "instruction accounts must be a JSON array".to_string(), + ); + }, + }; + if instruction_accounts.len() > crate::DC_METADATA_SPM_MAX_INSTRUCTION_ACCOUNTS { + return std::result::Result::Err( + "instruction account count exceeds the bounded maximum".to_string(), + ); + } + 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( + "transaction account keys must be a JSON array".to_string(), + ); + }, + }; + 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(format!( + "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(format!( + "instruction account {position} has no accountKey" + )); + }, + }; + let resolved = match account_keys.iter().find(|candidate| { + return candidate.get("accountIndex").and_then(serde_json::Value::as_u64) + == std::option::Option::Some(account_index); + }) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "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(format!( + "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(format!( + "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(format!( + "account index {account_index} has no writable flag" + )); + }, + }; + output.push(ResolvedAccount { + position, + account_index, + account_key: account_key.to_string(), + signer, + writable, + source: match resolved.get("source") { + std::option::Option::Some(value) => value.clone(), + std::option::Option::None => serde_json::Value::Null, + }, + }); + } + return std::result::Result::Ok(output); +} + +fn validate_accounts( + parsed: &ParsedInstruction, + accounts: &[ResolvedAccount], +) -> std::result::Result<(), std::string::String> { + let (contracts, allow_remaining) = account_contract(parsed.code); + if allow_remaining { + if accounts.len() < contracts.len() { + return std::result::Result::Err(format!( + "{} requires at least {} positional accounts; received {}", + parsed.code, + contracts.len(), + accounts.len() + )); + } + } else if accounts.len() != contracts.len() { + return std::result::Result::Err(format!( + "{} requires exactly {} positional accounts; received {}", + parsed.code, + contracts.len(), + accounts.len() + )); + } + for (position, contract) in contracts.iter().enumerate() { + let account = match accounts.get(position) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "{} is missing account {position}", + parsed.code + )); + }, + }; + if account.signer != contract.signer || account.writable != contract.writable { + return std::result::Result::Err(format!( + "{} account {position} ({}) has invalid signer/writable flags", + parsed.code, contract.role + )); + } + } + if let std::result::Result::Err(message) = validate_source_contract(parsed, accounts) { + return std::result::Result::Err(message); + } + return std::result::Result::Ok(()); +} + +fn validate_source_contract( + parsed: &ParsedInstruction, + accounts: &[ResolvedAccount], +) -> std::result::Result<(), std::string::String> { + match parsed.source_contract { + SourceContract::None => return std::result::Result::Ok(()), + SourceContract::Write { inline_data_length } => { + let source_present = optional_account_present(accounts, 2); + if inline_data_length > 0 && source_present { + return std::result::Result::Err( + "write cannot provide both inline data and a source buffer".to_string(), + ); + } + if inline_data_length == 0 && !source_present { + return std::result::Result::Err( + "write requires inline data or a source buffer".to_string(), + ); + } + }, + SourceContract::SetDataPreserve => { + if optional_account_present(accounts, 2) { + return std::result::Result::Err( + "set_data header-only variant requires the buffer placeholder".to_string(), + ); + } + }, + SourceContract::SetDataBuffer => { + if !optional_account_present(accounts, 2) { + return std::result::Result::Err( + "set_data without inline bytes requires a source buffer".to_string(), + ); + } + }, + SourceContract::SetDataInline { inline_data_length } => { + if inline_data_length == 0 || optional_account_present(accounts, 2) { + return std::result::Result::Err( + "set_data inline variant requires bytes and the buffer placeholder".to_string(), + ); + } + }, + SourceContract::Allocate { seed_present } => { + let buffer = &accounts[0].account_key; + let authority = &accounts[1].account_key; + if seed_present && buffer == authority { + return std::result::Result::Err( + "allocate PDA variant requires distinct buffer and authority accounts" + .to_string(), + ); + } + if !seed_present && buffer != authority { + return std::result::Result::Err( + "allocate keypair variant requires buffer and authority to be the same account" + .to_string(), + ); + } + }, + } + return std::result::Result::Ok(()); +} + +fn optional_account_present(accounts: &[ResolvedAccount], position: usize) -> bool { + return accounts.get(position).is_some_and(|account| { + return account.account_key != kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID; + }); +} + +fn account_contract( + code: &str, +) -> (&'static [crate::DcMetadataSpmInstructionAccountContract], bool) { + return match code { + "write" => (crate::DC_METADATA_SPM_WRITE_ACCOUNT_CONTRACTS, true), + "initialize" => (crate::DC_METADATA_SPM_INITIALIZE_ACCOUNT_CONTRACTS, true), + "set_authority" => (crate::DC_METADATA_SPM_SET_AUTHORITY_ACCOUNT_CONTRACTS, false), + "set_data" => (crate::DC_METADATA_SPM_SET_DATA_ACCOUNT_CONTRACTS, true), + "set_immutable" => (crate::DC_METADATA_SPM_SET_IMMUTABLE_ACCOUNT_CONTRACTS, false), + "trim" => (crate::DC_METADATA_SPM_TRIM_ACCOUNT_CONTRACTS, false), + "close" => (crate::DC_METADATA_SPM_CLOSE_ACCOUNT_CONTRACTS, false), + "allocate" => (crate::DC_METADATA_SPM_ALLOCATE_ACCOUNT_CONTRACTS, true), + "extend" => (crate::DC_METADATA_SPM_EXTEND_ACCOUNT_CONTRACTS, true), + _ => (&[], false), + }; +} + +fn accounts_json(code: &str, accounts: &[ResolvedAccount]) -> serde_json::Value { + let (contracts, _) = account_contract(code); + return serde_json::Value::Array( + accounts + .iter() + .map(|account| { + let contract = contracts.get(account.position); + let optional = contract.is_some_and(|value| return value.optional); + let present = !optional + || account.account_key + != kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID; + let role = match contract { + std::option::Option::Some(value) => value.role, + std::option::Option::None => "remaining_account", + }; + return serde_json::json!({ + "position": account.position, + "accountIndex": account.account_index, + "accountKey": account.account_key, + "signer": account.signer, + "writable": account.writable, + "source": account.source, + "role": role, + "optional": optional, + "present": present, + "placeholderProgramId": optional && !present, + }); + }) + .collect(), + ); +} + +fn observation( + input: &crate::MdCoreInstructionReplayInput, + parsed: &ParsedInstruction, + accounts: serde_json::Value, + wire: &[u8], +) -> crate::DcApiDecodedObservation { + let wire_hash = hash(wire); + let committed = !input.transaction_failed; + let event = crate::MdDecodedProtocolEvent { + signature: crate::MdSignature(input.signature.clone()), + slot: crate::MdSlot(input.slot), + instruction_path: crate::MdInstructionPath(input.instruction_path.clone()), + program_id: crate::MdProgramId(input.program_id.clone()), + protocol_code: crate::MdProtocolCode(crate::DC_METADATA_SPM_SURFACE_CODE.to_string()), + surface_code: crate::MdSurfaceCode(crate::DC_METADATA_SPM_SURFACE_CODE.to_string()), + event_code: crate::MdEventCode(format!( + "{}.{}", + crate::DC_METADATA_SPM_SURFACE_CODE, + parsed.code + )), + event_name: crate::MdEventName(parsed.code.to_string()), + event_family: crate::MdEventFamily::Metadata, + source_kind: if input.instruction_path.contains('/') { + crate::MdEventSourceKind::InnerInstruction + } else { + crate::MdEventSourceKind::Instruction + }, + confidence: crate::MdDecoderConfidence::ManualExact, + }; + return crate::DcApiDecodedObservation { + event_key: format!("solana_program_metadata:{}:0", parsed.code), + event, + payload_json: serde_json::json!({ + "eventVersion": crate::DC_METADATA_SPM_EVENT_VERSION, + "programId": input.program_id, + "instruction": parsed.code, + "wireTag": parsed.tag, + "wireTagHex": format!("{:02x}", parsed.tag), + "instructionPath": input.instruction_path, + "instructionLocation": if input.instruction_path.contains('/') { "inner" } else { "outer" }, + "transactionSucceeded": !input.transaction_failed, + "committed": committed, + "parameters": parsed.parameters, + "accounts": accounts, + "wire": { + "lengthBytes": wire.len(), + "sha256": wire_hash, + "prefixHex": hex_prefix(wire), + "runtimeIgnoredSuffixLengthBytes": parsed.runtime_ignored_suffix_length, + "complete": true, + }, + "history": { + "stableProgramRelease": "program@v1.0.1", + "stableDiscriminatorInventory": "program@v1.0.0_and_program@v1.0.1", + "preStableNameAtTag5": if parsed.tag == 5 { serde_json::Value::String("withdraw_excess_lamports".to_string()) } else { serde_json::Value::Null }, + "preStableNameDecodedSeparately": false, + }, + "programBoundary": { + "solanaProgramMetadataProgramId": kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID, + "metaplexTokenMetadataProgramId": kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + "token2022ProgramId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, + "offchainContentFetched": false, + }, + }), + transaction_failed: input.transaction_failed, + transaction_error: input.transaction_err_json.clone(), + observation_committed: committed, + proof: crate::DcApiDecoderProof { + kind: crate::DcApiDecoderProofKind::Manual, + confidence: crate::MdDecoderConfidence::ManualExact, + evidence: std::vec![ + "solana_program_metadata_program_1_0_1_processor_wire".to_string(), + "solana_program_metadata_codama_idl_v0_0_0".to_string(), + format!("wire_tag:{}", parsed.tag), + format!("wire_sha256:{wire_hash}"), + ], + }, + }; +} + +fn data_summary(bytes: &[u8]) -> serde_json::Value { + return serde_json::json!({ + "lengthBytes": bytes.len(), + "sha256": hash(bytes), + "base64": base64::engine::general_purpose::STANDARD.encode(bytes), + }); +} + +fn byte_summary(bytes: &[u8]) -> serde_json::Value { + return serde_json::json!({ + "lengthBytes": bytes.len(), + "sha256": hash(bytes), + "prefixHex": hex_prefix(bytes), + }); +} + +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::DC_METADATA_SPM_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::DcApiDecoderExecutionResult { + return crate::DcApiDecoderExecutionResult { + status: crate::DcApiDecoderOutcomeStatus::Failed, + recognized_entry_code: entry_code.map(str::to_string), + observations: std::vec::Vec::new(), + diagnostics: std::vec![crate::DcApiDecoderDiagnostic { + code: code.to_string(), + message, + retriable: false, + }], + }; +} + +#[cfg(test)] +mod tests { + use base64::Engine; // rust-rules: trait-import + + const MATRIX_JSON: &str = include_str!( + "../../../../../test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json" + ); + + fn input( + payload: &[u8], + accounts: &[(&str, bool, bool)], + failed: bool, + path: &str, + ) -> crate::MdCoreInstructionReplayInput { + let keys = accounts + .iter() + .enumerate() + .map(|(position, (key, signer, writable))| { + return serde_json::json!({ + "accountIndex": position, + "accountKey": key, + "source": "static", + "signer": signer, + "writable": writable, + }); + }) + .collect::>(); + let instruction_accounts = accounts + .iter() + .enumerate() + .map(|(position, (key, _, _))| { + return serde_json::json!({"accountIndex": position, "accountKey": key}); + }) + .collect::>(); + let result = crate::MdCoreInstructionReplayInput::new( + format!("signature:{path}"), + "signature", + 42, + path, + kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID, + failed, + if failed { + std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]})) + } else { + std::option::Option::None + }, + serde_json::Value::Array(keys), + serde_json::Value::Array(instruction_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!("Solana Program Metadata replay input failed: {error}") + }, + }; + } + + fn placeholder() -> &'static str { + return kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID; + } + + fn decode( + payload: &[u8], + accounts: &[(&str, bool, bool)], + ) -> crate::DcApiDecoderExecutionResult { + let input = input(payload, accounts, false, "0"); + return crate::DcApiInstructionDecoder::decode( + &crate::DcMetadataSolanaProgramMetadataDecoder, + &input, + ); + } + + #[test] + fn stable_inventory_and_historical_name_boundary_are_exact() { + let decoder = crate::DcMetadataSolanaProgramMetadataDecoder; + let coverage = crate::DcApiInstructionDecoder::coverage(&decoder); + assert_eq!(coverage.len(), 9); + assert!(coverage.iter().all(|entry| return !entry.historical)); + assert_eq!(coverage[5].entry_code, "trim"); + assert_eq!(coverage[5].discriminator_hex.as_deref(), std::option::Option::Some("05")); + let trim = decode( + &[5, 9, 8], + &[ + ("account", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ("destination", false, true), + ("SysvarRent111111111111111111111111111111111", false, false), + ], + ); + assert_eq!(trim.status, crate::DcApiDecoderOutcomeStatus::Decoded); + assert_eq!(trim.observations[0].payload_json["instruction"], "trim"); + assert_eq!( + trim.observations[0].payload_json["history"]["preStableNameAtTag5"], + "withdraw_excess_lamports" + ); + assert_eq!(trim.observations[0].payload_json["wire"]["runtimeIgnoredSuffixLengthBytes"], 2); + } + + #[test] + fn current_nine_instruction_wires_decode_with_exact_parameters() { + let external = [7_u8; crate::DC_METADATA_SPM_EXTERNAL_DATA_BYTES]; + let cases = [ + ( + [vec![0, 4, 0, 0, 0], vec![1, 2]].concat(), + vec![ + ("buffer", false, true), + ("authority", true, false), + (placeholder(), false, false), + ], + "write", + ), + ( + [vec![1], vec![0; 16], vec![1, 0, 1, 2], external.to_vec()].concat(), + vec![ + ("metadata", false, true), + ("authority", true, false), + ("program", false, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "initialize", + ), + ( + [vec![2, 1], vec![3; 32]].concat(), + vec![ + ("account", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "set_authority", + ), + ( + [vec![3, 1, 0, 1, 0], vec![1]].concat(), + vec![ + ("metadata", false, true), + ("authority", true, false), + (placeholder(), false, true), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "set_data", + ), + ( + vec![4], + vec![ + ("metadata", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "set_immutable", + ), + ( + vec![5], + vec![ + ("account", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ("destination", false, true), + ("SysvarRent111111111111111111111111111111111", false, false), + ], + "trim", + ), + ( + vec![6], + vec![ + ("account", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ("destination", false, true), + ], + "close", + ), + ( + [vec![7], vec![8; 16]].concat(), + vec![ + ("buffer", false, true), + ("authority", true, false), + ("program", false, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "allocate", + ), + ( + vec![8, 32, 0], + vec![ + ("account", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + "extend", + ), + ]; + for (payload, accounts, expected) in cases { + let result = decode(payload.as_slice(), accounts.as_slice()); + assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Decoded, "{expected}"); + assert_eq!( + result.recognized_entry_code.as_deref(), + std::option::Option::Some(expected) + ); + } + } + + #[test] + fn processor_variants_and_runtime_ignored_suffixes_are_preserved() { + let preserve = decode( + &[3, 1, 0, 1], + &[ + ("metadata", false, true), + ("authority", true, false), + (placeholder(), false, true), + (placeholder(), false, false), + (placeholder(), false, false), + ], + ); + assert_eq!(preserve.status, crate::DcApiDecoderOutcomeStatus::Decoded); + assert_eq!( + preserve.observations[0].payload_json["parameters"]["dataMutation"], + "preserve_existing_data" + ); + let remove_with_suffix = decode( + &[2, 0, 9, 8, 7], + &[ + ("account", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + ); + assert_eq!(remove_with_suffix.status, crate::DcApiDecoderOutcomeStatus::Decoded); + assert_eq!( + remove_with_suffix.observations[0].payload_json["wire"]["runtimeIgnoredSuffixLengthBytes"], + 3 + ); + for tag in [4_u8, 5_u8, 6_u8] { + let accounts = match tag { + 4 => vec![ + ("metadata", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + 5 => vec![ + ("account", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ("destination", false, true), + ("SysvarRent111111111111111111111111111111111", false, false), + ], + _ => vec![ + ("account", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ("destination", false, true), + ], + }; + let result = decode(&[tag, 1, 2, 3], accounts.as_slice()); + assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Decoded); + } + } + + #[test] + fn source_selection_and_account_flags_fail_closed() { + let invalid_write = decode( + &[0, 0, 0, 0, 0, 1], + &[("buffer", false, true), ("authority", true, false), ("source", false, false)], + ); + assert_eq!(invalid_write.status, crate::DcApiDecoderOutcomeStatus::Failed); + let invalid_set_data = decode( + &[3, 1, 0, 1, 0, 1], + &[ + ("metadata", false, true), + ("authority", true, false), + ("source", false, true), + (placeholder(), false, false), + (placeholder(), false, false), + ], + ); + assert_eq!(invalid_set_data.status, crate::DcApiDecoderOutcomeStatus::Failed); + let invalid_allocate = decode( + &[7], + &[ + ("buffer", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + ); + assert_eq!(invalid_allocate.status, crate::DcApiDecoderOutcomeStatus::Failed); + let invalid_flags = decode( + &[8, 1, 0], + &[ + ("account", false, false), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + ); + assert_eq!(invalid_flags.status, crate::DcApiDecoderOutcomeStatus::Failed); + } + + #[test] + fn malformed_unknown_and_invalid_enum_wires_fail_closed() { + let base_accounts = [ + ("account", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ]; + for payload in [vec![], vec![9], vec![8, 1], vec![2], vec![2, 1, 0], vec![7, 1], vec![1, 0]] + { + let input = input(payload.as_slice(), &base_accounts, false, "0"); + let result = crate::DcApiInstructionDecoder::decode( + &crate::DcMetadataSolanaProgramMetadataDecoder, + &input, + ); + assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Failed); + } + let initialize_accounts = [ + ("metadata", false, true), + ("authority", true, false), + ("program", false, false), + (placeholder(), false, false), + (placeholder(), false, false), + ]; + for invalid_tail in [[99_u8, 0, 0, 1], [0, 99, 0, 1], [0, 0, 99, 1], [0, 0, 0, 99]] { + let invalid_enum_payload = + [vec![1], vec![0; crate::DC_METADATA_SPM_SEED_BYTES], invalid_tail.to_vec()] + .concat(); + let invalid_enum = decode(invalid_enum_payload.as_slice(), &initialize_accounts); + assert_eq!(invalid_enum.status, crate::DcApiDecoderOutcomeStatus::Failed); + } + } + + #[test] + fn failed_transactions_remain_exact_uncommitted_intents() { + let input = input( + &[8, 1, 0], + &[ + ("account", false, true), + ("authority", true, false), + (placeholder(), false, false), + (placeholder(), false, false), + ], + true, + "1/0", + ); + let result = crate::DcApiInstructionDecoder::decode( + &crate::DcMetadataSolanaProgramMetadataDecoder, + &input, + ); + assert_eq!(result.status, crate::DcApiDecoderOutcomeStatus::Decoded); + assert!(result.observations[0].transaction_failed); + assert!(!result.observations[0].observation_committed); + } + + #[test] + fn instruction_matrix_is_machine_readable_and_matches_compiled_inventory() { + let matrix: serde_json::Value = match serde_json::from_str(MATRIX_JSON) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("instruction matrix must parse: {error}"), + }; + let entries = match matrix.get("instructions").and_then(serde_json::Value::as_array) { + std::option::Option::Some(value) => value, + std::option::Option::None => panic!("instruction matrix must contain instructions"), + }; + assert_eq!(entries.len(), crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES.len()); + for (position, (tag, code)) in crate::DC_METADATA_SPM_INSTRUCTION_ENTRIES.iter().enumerate() + { + assert_eq!(entries[position]["tag"], serde_json::json!(tag)); + assert_eq!(entries[position]["code"], serde_json::json!(code)); + } + assert_eq!(matrix["historyAudit"]["obsoleteUniqueDiscriminators"], 0); + assert_eq!(matrix["historyAudit"]["renamedTag5PreStable"], "withdraw_excess_lamports"); + } +} diff --git a/kb-lib/src/lib.rs b/kb-lib/src/lib.rs index 6e7ad65..7f76793 100644 --- a/kb-lib/src/lib.rs +++ b/kb-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/lib.rs -// version: 31 +// version: 32 //! Consolidated decoder, executor, materializer and shared model library. #![warn(missing_docs)] @@ -29,7 +29,6 @@ pub use self::decoder::DC_METADATA_SPM_SEED_BYTES; pub use self::decoder::DC_SPL_ELGAMAL_REGISTRY_ACCOUNT_LEN; /// Reserved `kb-lib.decoder.adapter.saber_decimal_wrapper` decoder. pub use self::decoder::DcAdapterSaberDecimalWrapperDecoder; -/// Approve Collection Authority discriminator. /// Exposes the reserved `DcAdapterSplTokenWrapDecoder` implementation. pub use self::decoder::DcAdapterSplTokenWrapDecoder; /// Reserved `kb-lib.decoder.amm.aldrin_v1` decoder. @@ -1641,6 +1640,40 @@ pub(crate) use self::decoder::DC_METADATA_MTM_VERIFY_COLLECTION_DISCRIMINATOR; pub(crate) use self::decoder::DC_METADATA_MTM_VERIFY_DISCRIMINATOR; /// Verify Sized Collection Item discriminator. pub(crate) use self::decoder::DC_METADATA_MTM_VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR; +/// Account contract for `Allocate`. +pub(crate) use self::decoder::DC_METADATA_SPM_ALLOCATE_ACCOUNT_CONTRACTS; +/// Account contract for `Close`. +pub(crate) use self::decoder::DC_METADATA_SPM_CLOSE_ACCOUNT_CONTRACTS; +/// Surfaces handled by the Solana Program Metadata decoder. +pub(crate) use self::decoder::DC_METADATA_SPM_DECODER_SURFACES; +/// Maximum instruction prefix retained in hexadecimal diagnostics. +pub(crate) use self::decoder::DC_METADATA_SPM_DIAGNOSTIC_PREFIX_BYTES; +/// Stable event schema version for decoded Solana Program Metadata instructions. +pub(crate) use self::decoder::DC_METADATA_SPM_EVENT_VERSION; +/// Account contract for `Extend`. +pub(crate) use self::decoder::DC_METADATA_SPM_EXTEND_ACCOUNT_CONTRACTS; +/// Account contract for `Initialize`. +pub(crate) use self::decoder::DC_METADATA_SPM_INITIALIZE_ACCOUNT_CONTRACTS; +/// Current stable instruction inventory for program releases 1.0.0 and 1.0.1. +pub(crate) use self::decoder::DC_METADATA_SPM_INSTRUCTION_ENTRIES; +/// Maximum instruction-account count accepted by the contextual decoder. +pub(crate) use self::decoder::DC_METADATA_SPM_MAX_INSTRUCTION_ACCOUNTS; +/// Maximum retained instruction payload accepted by the contextual decoder. +pub(crate) use self::decoder::DC_METADATA_SPM_MAX_INSTRUCTION_BYTES; +/// Program ids handled by the Solana Program Metadata decoder. +pub(crate) use self::decoder::DC_METADATA_SPM_PROGRAM_IDS; +/// Account contract for `SetAuthority`. +pub(crate) use self::decoder::DC_METADATA_SPM_SET_AUTHORITY_ACCOUNT_CONTRACTS; +/// Account contract for `SetData`. +pub(crate) use self::decoder::DC_METADATA_SPM_SET_DATA_ACCOUNT_CONTRACTS; +/// Account contract for `SetImmutable`. +pub(crate) use self::decoder::DC_METADATA_SPM_SET_IMMUTABLE_ACCOUNT_CONTRACTS; +/// Stable Solana Program Metadata decoder surface code. +pub(crate) use self::decoder::DC_METADATA_SPM_SURFACE_CODE; +/// Account contract for `Trim`. +pub(crate) use self::decoder::DC_METADATA_SPM_TRIM_ACCOUNT_CONTRACTS; +/// Account contract for `Write`. +pub(crate) use self::decoder::DC_METADATA_SPM_WRITE_ACCOUNT_CONTRACTS; /// Stable Address Lookup Table surface code. pub(crate) use self::decoder::DC_SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE; /// Stable deprecated immutable BPF Loader surface code. @@ -1765,6 +1798,8 @@ pub(crate) use self::decoder::DC_SPL_TOKEN_MAX_BATCH_INSTRUCTIONS; pub(crate) use self::decoder::DC_SPL_TOKEN_MAX_INSTRUCTION_BYTES; /// Stable protocol and surface code for the classic SPL Token program. pub(crate) use self::decoder::DC_SPL_TOKEN_SURFACE_CODE; +/// Expected account role and privileges for one instruction position. +pub(crate) use self::decoder::DcMetadataSpmInstructionAccountContract; /// Expected role and privileges for one positional instruction account. pub(crate) use self::decoder::DcSolanaCoreAccountRole; /// One resolved outer instruction payload and its provenance relative to the target instruction. @@ -1773,6 +1808,8 @@ pub(crate) use self::decoder::DcSolanaCoreResolvedInstructionPayload; pub(crate) use self::decoder::DcSplElGamalRegistryParsedRegistryInstruction; /// Canonical tracing target for this crate. pub(crate) use self::decoder::TRACING_TARGET_DECODER_METADATA_METAPLEX_TOKEN_METADATA; +/// Canonical tracing target for the Solana Program Metadata decoder. +pub(crate) use self::decoder::TRACING_TARGET_DECODER_METADATA_SOLANA_PROGRAM_METADATA; /// Canonical tracing target for this crate. pub(crate) use self::decoder::TRACING_TARGET_DECODER_SOLANA_CORE; /// Canonical tracing target for the Associated Token Account decoder. @@ -1791,6 +1828,12 @@ pub(crate) use self::decoder::decoder_metadata_metaplex_token_metadata_decode; pub(crate) use self::decoder::decoder_metadata_metaplex_token_metadata_entry_for_discriminator; /// Crate-root access to `payload_discriminator` from `instruction`. pub(crate) use self::decoder::decoder_metadata_metaplex_token_metadata_payload_discriminator; +/// Decodes one bounded Solana Program Metadata instruction. +pub(crate) use self::decoder::decoder_metadata_solana_program_metadata_decode; +/// Returns the stable entry matching one Solana Program Metadata discriminator. +pub(crate) use self::decoder::decoder_metadata_solana_program_metadata_entry_for_discriminator; +/// Returns the first byte of one retained Solana Program Metadata payload. +pub(crate) use self::decoder::decoder_metadata_solana_program_metadata_payload_discriminator; /// Returns declared Address Lookup Table instruction coverage. pub(crate) use self::decoder::decoder_solana_core_address_lookup_table_coverage; /// Decodes one Address Lookup Table instruction. diff --git a/kb-lib/tests/external_solana_program_metadata_instruction_api.rs b/kb-lib/tests/external_solana_program_metadata_instruction_api.rs new file mode 100644 index 0000000..42d0b9d --- /dev/null +++ b/kb-lib/tests/external_solana_program_metadata_instruction_api.rs @@ -0,0 +1,81 @@ +// file: kb-lib/tests/external_solana_program_metadata_instruction_api.rs +// version: 1 + +//! Downstream-style compilation contract for the Solana Program Metadata instruction decoder. + +#[test] +fn external_consumers_decode_instructions_only_through_crate_root_exports() { + let account = solana_pubkey::Pubkey::new_from_array([31_u8; 32]).to_string(); + let authority = solana_pubkey::Pubkey::new_from_array([32_u8; 32]).to_string(); + let placeholder = kb_program_ids::METADATA_SOLANA_PROGRAM_METADATA_PROGRAM_ID; + let account_keys = serde_json::json!([ + { + "accountIndex": 0, + "accountKey": account, + "source": "static", + "signer": false, + "writable": true + }, + { + "accountIndex": 1, + "accountKey": authority, + "source": "static", + "signer": true, + "writable": false + }, + { + "accountIndex": 2, + "accountKey": placeholder, + "source": "static", + "signer": false, + "writable": false + }, + { + "accountIndex": 3, + "accountKey": placeholder, + "source": "static", + "signer": false, + "writable": false + } + ]); + let instruction_accounts = serde_json::json!([ + {"accountIndex": 0, "accountKey": account}, + {"accountIndex": 1, "accountKey": authority}, + {"accountIndex": 2, "accountKey": placeholder}, + {"accountIndex": 3, "accountKey": placeholder} + ]); + let input_result = kb_lib::MdCoreInstructionReplayInput::new( + "spm:external:extend", + "signature", + 42, + "0", + placeholder, + false, + std::option::Option::None, + account_keys, + instruction_accounts, + std::option::Option::Some(serde_json::json!({"dataBase64": "CAEA"})), + std::option::Option::None, + serde_json::json!([]), + serde_json::json!([]), + serde_json::json!([]), + serde_json::json!([]), + ); + assert!(input_result.is_ok()); + let input: kb_lib::MdCoreInstructionReplayInput; + if let std::result::Result::Ok(value) = input_result { + input = value; + } else { + return; + } + let decoder = kb_lib::DcMetadataSolanaProgramMetadataDecoder; + let recognition = kb_lib::DcApiInstructionDecoder::recognize(&decoder, &input); + assert!(recognition.compatible); + assert!(recognition.exact); + assert_eq!(recognition.entry_code.as_deref(), std::option::Option::Some("extend")); + let result = kb_lib::DcApiInstructionDecoder::decode(&decoder, &input); + assert_eq!(result.status, kb_lib::DcApiDecoderOutcomeStatus::Decoded); + assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some("extend")); + assert_eq!(result.observations.len(), 1); + assert_eq!(result.observations[0].payload_json["parameters"]["length"], 1); +} diff --git a/test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json b/test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json new file mode 100644 index 0000000..eec39bb --- /dev/null +++ b/test-fixtures/contract-matrices/SOLANA_PROGRAM_METADATA_INSTRUCTION_MATRIX.json @@ -0,0 +1,111 @@ +{ + "schemaVersion": 1, + "surface": "metadata.solana_program_metadata", + "programId": "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S", + "stableProgramReleases": [ + "program@v1.0.0", + "program@v1.0.1" + ], + "sourceOfTruth": { + "runtime": "solana-program/program-metadata program@v1.0.1 processor sources", + "sdkContract": "Codama IDL version 0.0.0 and generated client", + "localIdl": "idls/metadata.ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S.solana_program_metadata.V0_0_0.from_github_solana_program.json" + }, + "instructions": [ + { + "tag": 0, + "code": "write", + "stable": true, + "body": "u32 offset followed by optional remainder bytes", + "positionalAccounts": 3, + "remainingAccountsAcceptedByRuntime": true, + "runtimeCompatibility": "exactly one source: inline remainder bytes or source-buffer account" + }, + { + "tag": 1, + "code": "initialize", + "stable": true, + "body": "16-byte seed, encoding, compression, format, data-source, optional remainder bytes", + "positionalAccounts": 5, + "remainingAccountsAcceptedByRuntime": true, + "runtimeCompatibility": "empty remainder is valid only when the metadata PDA is a preallocated Buffer" + }, + { + "tag": 2, + "code": "set_authority", + "stable": true, + "body": "u8 option flag followed by zero or 32 public-key bytes", + "positionalAccounts": 4, + "remainingAccountsAcceptedByRuntime": false, + "runtimeCompatibility": "flag zero removes a canonical Metadata authority and ignores suffix bytes; Buffer authority removal fails at runtime" + }, + { + "tag": 3, + "code": "set_data", + "stable": true, + "body": "encoding, compression, format, optional data-source and optional remainder bytes", + "positionalAccounts": 5, + "remainingAccountsAcceptedByRuntime": true, + "runtimeCompatibility": "three-byte processor form preserves existing data; otherwise exactly one source is selected between inline bytes and Buffer" + }, + { + "tag": 4, + "code": "set_immutable", + "stable": true, + "body": "no SDK arguments", + "positionalAccounts": 4, + "remainingAccountsAcceptedByRuntime": false, + "runtimeCompatibility": "processor ignores instruction suffix bytes" + }, + { + "tag": 5, + "code": "trim", + "stable": true, + "body": "no SDK arguments", + "positionalAccounts": 6, + "remainingAccountsAcceptedByRuntime": false, + "runtimeCompatibility": "processor ignores instruction suffix bytes" + }, + { + "tag": 6, + "code": "close", + "stable": true, + "body": "no SDK arguments", + "positionalAccounts": 5, + "remainingAccountsAcceptedByRuntime": false, + "runtimeCompatibility": "processor ignores instruction suffix bytes" + }, + { + "tag": 7, + "code": "allocate", + "stable": true, + "body": "empty for a keypair Buffer or exactly one 16-byte PDA seed", + "positionalAccounts": 5, + "remainingAccountsAcceptedByRuntime": true, + "runtimeCompatibility": "keypair form requires Buffer and authority to be the same account; PDA form requires distinct accounts" + }, + { + "tag": 8, + "code": "extend", + "stable": true, + "body": "u16 little-endian length", + "positionalAccounts": 4, + "remainingAccountsAcceptedByRuntime": true, + "runtimeCompatibility": "current runtime contract" + } + ], + "historyAudit": { + "obsoleteUniqueDiscriminators": 0, + "renamedTag5PreStable": "withdraw_excess_lamports", + "renamedTag5Current": "trim", + "tag5Discriminator": 5, + "duplicateHistoricalCoverageEntry": false, + "preStableWipWireLayouts": "not_declared_without_cluster_evidence", + "decision": "decode tag 5 as the stable Trim contract and retain the pre-stable name only as provenance" + }, + "surfaceBoundaries": { + "metaplexTokenMetadata": "separate program and decoder", + "token2022TokenMetadata": "separate Token-2022 interface implementation", + "offchainResolution": false + } +}