From 51ed3d11cf1d799e4f46bba9773463915163495e Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Thu, 23 Jul 2026 21:19:45 +0200 Subject: [PATCH] v0.1.0-pre.009 --- README.md | 4 +- ROADMAP.md | 20 +- config/example.config.json | 60 +- docs/METAPLEX_TOKEN_METADATA_MATRIX.json | 52 +- docs/PROGRAM_REGISTRY_CONTROL.md | 4 +- docs/SOLANA_INTERFACE_DEPENDENCIES.md | 19 +- docs/TRACING_CONTRACT.md | 3 +- kb-lib/Cargo.toml | 5 +- kb-lib/README.md | 22 +- kb-lib/src/decoder.rs | 4 +- kb-lib/src/decoder/metadata.rs | 5 +- .../metadata/metaplex_token_metadata.rs | 225 +- .../metaplex_token_metadata/account.rs | 2631 ++++++++++++++ .../metaplex_token_metadata/canonical.rs | 425 +++ .../metaplex_token_metadata/constants.rs | 257 ++ .../metaplex_token_metadata/decoder.rs | 3152 +++++++++++++++++ .../metaplex_token_metadata/instruction.rs | 2746 ++++++++++++++ kb-lib/src/lib.rs | 4 +- kb-program-ids/README.md | 4 +- kb-program-ids/src/constants.rs | 9 +- kb-program-ids/src/lib.rs | 4 +- kb-program-ids/src/programs.rs | 11 +- 22 files changed, 9580 insertions(+), 86 deletions(-) create mode 100644 kb-lib/src/decoder/metadata/metaplex_token_metadata/account.rs create mode 100644 kb-lib/src/decoder/metadata/metaplex_token_metadata/canonical.rs create mode 100644 kb-lib/src/decoder/metadata/metaplex_token_metadata/constants.rs create mode 100644 kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs create mode 100644 kb-lib/src/decoder/metadata/metaplex_token_metadata/instruction.rs diff --git a/README.md b/README.md index b11fafc..78885f3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - + # Khadhroony Bot3 @@ -56,3 +56,5 @@ cargo clippy --workspace --all-targets `0.1.0-pre.004` porte le décodeur Solana Core dans `kb-lib`. `0.1.0-pre.005` ajoute SPL Memo v1, v3 et v4. `0.1.0-pre.006` ajoute le décodeur SPL Token classique avec ses 28 tags publiés, son wire borné, ses formes d’autorité et son `Batch` structuré. `0.1.0-pre.007` ajoute le programme SPL Associated Token Account avec ses trois instructions, sa forme historique `Create` vide et la validation canonique des PDA pour SPL Token classique et Token‑2022. `0.1.0-pre.008` porte le décodeur Token‑2022 maximal, son parseur d’états Mint/Account/Multisig et de TLV, ainsi que le programme indépendant du registre ElGamal. Les deux composants gardent des frontières et des targets de tracing distincts. Tous les décodeurs migrés utilisent les mêmes contrats contextualisés, la même façade publique et aucune dépendance inverse vers `kb-store`. + +`0.1.0-pre.009` porte le décodeur indépendant Metaplex Token Metadata. Il couvre les 58 discriminateurs d’instructions publiés `0..=57`, les 15 variantes de comptes `Key 0..=14`, les PDA officiels et les layouts courants, expérimentaux et historiques. Les metadata externes Metaplex restent séparées des metadata incorporées de Token‑2022, de Metaplex Core et de Bubblegum. diff --git a/ROADMAP.md b/ROADMAP.md index c92c6f6..8f5468b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,5 @@ - + # ROADMAP — khadhroony-bot3 @@ -96,5 +96,19 @@ - [x] Déplacer `ProgramIdEntry` et les fonctions du registre dans `kb-program-ids/src/programs.rs`, avec `lib.rs` limité aux réexports. - [x] Porter les targets de tracing vers `kb-lib.decoder.spl.token_2022` et `kb-lib.decoder.spl.elgamal_registry`. - [x] Conserver les 55 tests Token‑2022/ElGamal et leurs matrices machine-readable. -- [ ] Valider `kb-program-ids`, les 251 tests de `kb-lib`, Clippy et le workspace avec Cargo sur la machine de développement. -- [ ] Poursuivre avec le décodeur Metaplex Token Metadata après validation. +- [x] Valider `kb-program-ids`, les 251 tests de `kb-lib`, Clippy et le workspace avec Cargo sur la machine de développement. +- [x] Poursuivre avec le décodeur Metaplex Token Metadata après validation. + +### 0.1.0-pre.009 — Décodeur Metaplex Token Metadata dans `kb-lib` + +- [x] Remplacer le scaffold `kb-lib::decoder::metadata::metaplex_token_metadata` par le décodeur maximal validé de bot2. +- [x] Conserver l’inventaire exhaustif des 58 discriminateurs d’instructions `0..=57`, y compris les variantes historiques. +- [x] Conserver l’inventaire exhaustif des 15 variantes de comptes `Key 0..=14`, avec rejet explicite de `Uninitialized`. +- [x] Porter les parseurs bornés de Metadata, Edition, Master Edition, Edition Marker, Token Record, records d’autorité et de délégation, escrow et Reservation List. +- [x] Préserver les validations d’owner, de PDA, de bump, de longueur, de suffixe, de comptes ordonnés et de provenance. +- [x] Maintenir les layouts historiques matérialisables avec lifecycle explicite mais définitivement non exécutables. +- [x] Ajouter le Program ID Metaplex Token Metadata à `kb-program-ids` sans le classer comme surface native. +- [x] Porter le target de tracing vers `kb-lib.decoder.metadata.metaplex_token_metadata`. +- [x] Conserver les 69 tests Metaplex et la matrice machine-readable exhaustive. +- [ ] Valider `kb-program-ids`, les 320 tests de `kb-lib`, Clippy et le workspace avec Cargo sur la machine de développement. +- [ ] Poursuivre avec les matérialisateurs et exécuteurs après validation du port des décodeurs prioritaires. diff --git a/config/example.config.json b/config/example.config.json index 2943cc1..3255c04 100644 --- a/config/example.config.json +++ b/config/example.config.json @@ -22,7 +22,7 @@ "ansi": true, "targets": [ "kb_app_demo", - "kb_decoder_metadata_metaplex_token_metadata", + "kb-lib.decoder.metadata.metaplex_token_metadata", "kb-lib.decoder.solana.core", "kb-lib.decoder.spl.associated_token_account", "kb-lib.decoder.spl.elgamal_registry", @@ -1161,42 +1161,42 @@ ] }, { - "name": "file_kb_decoder_metadata_metaplex_token_metadata_debug", + "name": "file_kb_lib_decoder_metadata_metaplex_token_metadata_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/devnet/kb_decoder_metadata_metaplex_token_metadata/debug.log", + "path": "logs/devnet/kb-lib/decoder/metadata/metaplex_token_metadata/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_decoder_metadata_metaplex_token_metadata" + "kb-lib.decoder.metadata.metaplex_token_metadata" ] }, { - "name": "file_kb_decoder_metadata_metaplex_token_metadata_info", + "name": "file_kb_lib_decoder_metadata_metaplex_token_metadata_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/devnet/kb_decoder_metadata_metaplex_token_metadata/info.log", + "path": "logs/devnet/kb-lib/decoder/metadata/metaplex_token_metadata/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_decoder_metadata_metaplex_token_metadata" + "kb-lib.decoder.metadata.metaplex_token_metadata" ] }, { - "name": "file_kb_decoder_metadata_metaplex_token_metadata_error", + "name": "file_kb_lib_decoder_metadata_metaplex_token_metadata_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/devnet/kb_decoder_metadata_metaplex_token_metadata/error.jsonl", + "path": "logs/devnet/kb-lib/decoder/metadata/metaplex_token_metadata/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb_decoder_metadata_metaplex_token_metadata" + "kb-lib.decoder.metadata.metaplex_token_metadata" ] }, { @@ -1624,7 +1624,7 @@ "ansi": true, "targets": [ "kb_app_demo", - "kb_decoder_metadata_metaplex_token_metadata", + "kb-lib.decoder.metadata.metaplex_token_metadata", "kb-lib.decoder.solana.core", "kb-lib.decoder.spl.associated_token_account", "kb-lib.decoder.spl.elgamal_registry", @@ -2763,42 +2763,42 @@ ] }, { - "name": "file_kb_decoder_metadata_metaplex_token_metadata_debug", + "name": "file_kb_lib_decoder_metadata_metaplex_token_metadata_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/mainnet_research/kb_decoder_metadata_metaplex_token_metadata/debug.log", + "path": "logs/mainnet_research/kb-lib/decoder/metadata/metaplex_token_metadata/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_decoder_metadata_metaplex_token_metadata" + "kb-lib.decoder.metadata.metaplex_token_metadata" ] }, { - "name": "file_kb_decoder_metadata_metaplex_token_metadata_info", + "name": "file_kb_lib_decoder_metadata_metaplex_token_metadata_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/mainnet_research/kb_decoder_metadata_metaplex_token_metadata/info.log", + "path": "logs/mainnet_research/kb-lib/decoder/metadata/metaplex_token_metadata/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_decoder_metadata_metaplex_token_metadata" + "kb-lib.decoder.metadata.metaplex_token_metadata" ] }, { - "name": "file_kb_decoder_metadata_metaplex_token_metadata_error", + "name": "file_kb_lib_decoder_metadata_metaplex_token_metadata_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/mainnet_research/kb_decoder_metadata_metaplex_token_metadata/error.jsonl", + "path": "logs/mainnet_research/kb-lib/decoder/metadata/metaplex_token_metadata/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb_decoder_metadata_metaplex_token_metadata" + "kb-lib.decoder.metadata.metaplex_token_metadata" ] }, { @@ -3426,7 +3426,7 @@ "ansi": true, "targets": [ "kb_app_demo", - "kb_decoder_metadata_metaplex_token_metadata", + "kb-lib.decoder.metadata.metaplex_token_metadata", "kb-lib.decoder.solana.core", "kb-lib.decoder.spl.associated_token_account", "kb-lib.decoder.spl.elgamal_registry", @@ -4565,42 +4565,42 @@ ] }, { - "name": "file_kb_decoder_metadata_metaplex_token_metadata_debug", + "name": "file_kb_lib_decoder_metadata_metaplex_token_metadata_debug", "enabled": true, "sink": "file", "level": "debug", - "path": "logs/mainnet/kb_decoder_metadata_metaplex_token_metadata/debug.log", + "path": "logs/mainnet/kb-lib/decoder/metadata/metaplex_token_metadata/debug.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_decoder_metadata_metaplex_token_metadata" + "kb-lib.decoder.metadata.metaplex_token_metadata" ] }, { - "name": "file_kb_decoder_metadata_metaplex_token_metadata_info", + "name": "file_kb_lib_decoder_metadata_metaplex_token_metadata_info", "enabled": true, "sink": "file", "level": "info", - "path": "logs/mainnet/kb_decoder_metadata_metaplex_token_metadata/info.log", + "path": "logs/mainnet/kb-lib/decoder/metadata/metaplex_token_metadata/info.log", "rotation": "daily", "format": "human", "ansi": false, "targets": [ - "kb_decoder_metadata_metaplex_token_metadata" + "kb-lib.decoder.metadata.metaplex_token_metadata" ] }, { - "name": "file_kb_decoder_metadata_metaplex_token_metadata_error", + "name": "file_kb_lib_decoder_metadata_metaplex_token_metadata_error", "enabled": true, "sink": "file", "level": "error", - "path": "logs/mainnet/kb_decoder_metadata_metaplex_token_metadata/error.jsonl", + "path": "logs/mainnet/kb-lib/decoder/metadata/metaplex_token_metadata/error.jsonl", "rotation": "daily", "format": "json", "ansi": false, "targets": [ - "kb_decoder_metadata_metaplex_token_metadata" + "kb-lib.decoder.metadata.metaplex_token_metadata" ] }, { diff --git a/docs/METAPLEX_TOKEN_METADATA_MATRIX.json b/docs/METAPLEX_TOKEN_METADATA_MATRIX.json index ecd2123..e0c5070 100644 --- a/docs/METAPLEX_TOKEN_METADATA_MATRIX.json +++ b/docs/METAPLEX_TOKEN_METADATA_MATRIX.json @@ -1,11 +1,11 @@ { "schema_version": 2, - "milestone": "0.4.7-pre.025", + "milestone": "0.1.0-pre.009", "surface_code": "metadata_metaplex_token_metadata", "program_id": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s", "program_family": "independent_metaplex_foundation_program", "decode_status": "instruction_inventory_exhaustive_0_through_57_and_account_key_inventory_exhaustive_0_through_14", - "runtime_registration": "not_enabled_in_pre_008", + "runtime_registration": "enabled_in_kb_lib_pre_009", "official_sources": [ { "kind": "repository", @@ -356,7 +356,7 @@ } ], "wire": "official_borsh_mpl_token_metadata_5_1_1", - "runtime_registration": "pending_full_group_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.003", @@ -392,7 +392,7 @@ ], "wire": "single_u8_discriminator_official_mpl_token_metadata_5_1_1", "account_contract": "ordered_required_accounts_plus_optional_collection_authority_record_and_bounded_remaining_accounts", - "runtime_registration": "pending_broader_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.004", @@ -428,7 +428,7 @@ ], "wire": "single_u8_discriminator_official_mpl_token_metadata_5_1_1", "account_contract": "ordered_required_accounts_plus_optional_collection_authority_record; collection metadata is writable and collection authority is readonly signer", - "runtime_registration": "pending_broader_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.005", @@ -597,7 +597,7 @@ "wire": "u8 discriminator + historical DataV2 + bool is_mutable using Borsh 0.10", "account_contract": "metadata, mint, mint_authority signer, payer writable signer, update_authority, system_program, optional rent", "provenance": "historical Metaplex Token Metadata instruction surface; current SDK DataV2 type reused only for exact compatible wire fields", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.010", @@ -624,7 +624,7 @@ ], "wire": "u8 discriminator + historical Data/Option fields using Borsh 0.10", "provenance": "historical generated Metaplex Token Metadata surface", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.011", @@ -651,7 +651,7 @@ ], "wire": "CreateMasterEditionV3: u8 discriminator + Option max_supply; conversion: u8 discriminator only", "account_contract": "exact generated Rust SDK account order and signer/writable flags; optional rent accepted for CreateMasterEditionV3", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.012", @@ -679,7 +679,7 @@ "wire": "u8 discriminator + MintNewEditionFromMasterEditionViaTokenArgs { edition: u64 }", "account_contract": "exact generated Rust SDK account order and signer/writable flags; optional rent accepted after the required program accounts", "edition_marker_contract": "edition marker PDA is writable and corresponds to floor(edition / EDITION_MARKER_BIT_SIZE); PDA derivation validation remains planned with account decoding", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.013", @@ -707,7 +707,7 @@ "wire": "u8 discriminator + SetCollectionSizeArgs { size: u64 }", "account_contract": "exact generated Rust SDK account order and signer/writable flags; optional collection authority record accepted", "provenance_boundary": "BubblegumSetCollectionSize is decoded as a Token Metadata bridge instruction and does not claim complete Bubblegum coverage", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.014", @@ -734,7 +734,7 @@ ], "wire": "u8 discriminator + DelegateArgs or RevokeArgs Borsh enum; DelegateArgs may contain optional AuthorizationData", "account_contract": "14 positional accounts with Metaplex program placeholders for absent optional accounts; exact signer/writable invariants on authority, payer, metadata and optional writable records", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.015", @@ -788,7 +788,7 @@ ], "wire": "Borsh enums TransferArgs/LockArgs/UnlockArgs/BurnArgs; CloseAccounts discriminator only", "account_contract": "exact generated Rust SDK positional accounts and signer/writable invariants; Metaplex program placeholders accepted for absent optional programmable accounts", - "runtime_registration": "pending_full_decoder_and_account_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.016", @@ -816,7 +816,7 @@ "wire": "u8 discriminator only", "account_contract": "delegate writable signer, token account writable, edition/mint/token program readonly", "inventory_audit": "official generated Rust SDK instruction module index audited; remaining modules are tracked before account-state decoding", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.017", @@ -852,7 +852,7 @@ ], "wire": "u8 discriminator only", "account_contract": "exact generated Rust SDK positional signer/writable contracts; BurnNft collection metadata, deprecated printing-token reservation list are optional trailing accounts", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.018", @@ -897,7 +897,7 @@ ], "wire": "Create/Close/Collect discriminator only; TransferOutOfEscrow discriminator + amount u64", "account_contract": "exact generated Rust SDK signer/writable contracts with optional trailing authority on create and transfer", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.019", @@ -915,7 +915,7 @@ ], "wire": "u8 discriminator + CreateArgs enum", "account_contract": "nine positional accounts with Metaplex placeholders for optional master edition and SPL Token Program", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.020", @@ -933,7 +933,7 @@ ], "wire": "u8 discriminator + MintArgs::V1 { amount: u64, authorization_data: Option }", "account_contract": "fifteen positional accounts with Metaplex placeholders for optional token owner, master edition, token record, delegate record and authorization-rule accounts", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.021", @@ -955,7 +955,7 @@ ], "wire": "u8 discriminator + PrintArgs::{V1,V2} { edition: u64 }", "account_contract": "eighteen positional accounts with a Metaplex placeholder or writable Token Record at position 6", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.022", @@ -984,7 +984,7 @@ ], "wire": "u8 discriminator + UpdateArgs enum", "account_contract": "eleven positional accounts with Metaplex placeholders for optional delegate record, token, edition and authorization-rule accounts", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.023", @@ -1031,7 +1031,7 @@ ], "wire": "u8 discriminator + UseArgs or VerificationArgs enum", "account_contract": "twelve positional accounts for Use; eight positional accounts for Verify/Unverify, with Metaplex placeholders for optional accounts", - "runtime_registration": "pending_full_decoder_validation" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "pre_release": "0.4.7-pre.024", @@ -1112,7 +1112,7 @@ ], "wire": "u8 discriminator only", "account_contract": "exact IDL/generated positional signer and writable flags", - "runtime_registration": "pending_full_decoder_validation", + "runtime_registration": "enabled_in_kb_lib_pre_009", "inventory_proof": { "covered_discriminators": [ 0, @@ -1198,7 +1198,7 @@ "uri_bytes": 200, "creators": 5 }, - "runtime_registration": "not_enabled" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "group": "edition_and_master_edition_accounts", @@ -1261,7 +1261,7 @@ "bit_order": "most_significant_bit_first" } }, - "runtime_registration": "not_enabled" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "group": "token_record_account", @@ -1300,7 +1300,7 @@ "LockedTransfer", "Migration" ], - "runtime_registration": "not_enabled" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "group": "delegate_and_authority_records", @@ -1363,7 +1363,7 @@ "holder_delegate_role_seeds": [ "print_delegate" ], - "runtime_registration": "not_enabled" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "group": "token_owned_escrow_account", @@ -1405,7 +1405,7 @@ "Migrate": "may create or update related programmable records; correlation pending", "Resize": "changes allocated account bytes without changing canonical identity" }, - "runtime_registration": "not_enabled" + "runtime_registration": "enabled_in_kb_lib_pre_009" }, { "group": "historical_reservation_lists", diff --git a/docs/PROGRAM_REGISTRY_CONTROL.md b/docs/PROGRAM_REGISTRY_CONTROL.md index 5afb9ab..036c144 100644 --- a/docs/PROGRAM_REGISTRY_CONTROL.md +++ b/docs/PROGRAM_REGISTRY_CONTROL.md @@ -1,5 +1,5 @@ - + # Contrôle du registre des programmes @@ -250,7 +250,7 @@ Ces identifiants sont détaillés dans `docs/CORE_PROGRAM_IDS.md`. Ils sont couv | `lock_raydium_lp` | `LockrWmn6K5twhz3y9w1dQERbmgSaRkfnTeTKbpofwE` | `raydium_lock_lp` | `raydium_lock_lp` | `-` | `kb_decoder_lock_raydium_lp` | `reserved_missing` | | `admin_jupiter_lock` | `LocpQgucEQHbqNABEYvBvwoxCPsSbG91A1QaQhQQqjn` | `jupiter_lock` | `jupiter_lock` | `-` | `kb_decoder_admin_jupiter_lock` | `reserved_missing` | | `admin_pump_fees` | `pfeeUxB6jkeY1Hxd7CsFCAjcbHA9rWtchMGdZ6VojVZ` | `pump_fees` | `pump_fees` | `kb_decoder_pump_fees` | `kb_decoder_admin_pump_fees` | `current_alias` | -| `metadata_metaplex_token_metadata` | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` | `metaplex_token_metadata` | `metaplex_token_metadata` | `-` | `kb_decoder_metadata_metaplex_token_metadata` | `reserved_missing` | +| `metadata_metaplex_token_metadata` | `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s` | `metaplex_token_metadata` | `metaplex_token_metadata` | `-` | `kb_lib::decoder::metadata::metaplex_token_metadata` | `current_migrated` | | `metadata_name_service` | `namesLPneVptA9Z5rqUDD9tMTWEJwofgaYwp8cawRkX` | `name_service` | `name_service` | `-` | `kb_decoder_metadata_spl_name_service` | `reserved_missing` | | `nft_metaplex_bubblegum` | `BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY` | `bubblegum` | `bubblegum` | `-` | `kb_decoder_nft_metaplex_bubblegum` | `reserved_missing` | | `nft_metaplex_mpl_core` | `CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d` | `mpl_core` | `mpl_core` | `-` | `kb_decoder_nft_metaplex_mpl_core` | `reserved_missing` | diff --git a/docs/SOLANA_INTERFACE_DEPENDENCIES.md b/docs/SOLANA_INTERFACE_DEPENDENCIES.md index fd69344..96b1062 100644 --- a/docs/SOLANA_INTERFACE_DEPENDENCIES.md +++ b/docs/SOLANA_INTERFACE_DEPENDENCIES.md @@ -1,5 +1,5 @@ - + # Dépendances d’interfaces Solana et SPL @@ -281,10 +281,23 @@ Pour chaque nouvelle interface : 6. tester les payloads tronqués, inconnus, suffixés et les comptes invalides ; 7. documenter toute divergence entre l’interface et le runtime. -### Metaplex Token Metadata — audit et premier décodage `0.4.7-pre.002` +### Metaplex Token Metadata — port maximal `0.1.0-pre.009` Le catalogue workspace déclare `mpl-token-metadata ^5.1` avec la seule feature `serde`. La publication officielle résolue reste `5.1.1`; elle fournit les modules générés `accounts`, `instructions`, `types` et `errors` issus de l’IDL Metaplex, sans introduire Anchor. La crate officielle utilise Borsh `< 1.0`; le décodeur référence donc explicitement `borsh 0.10` sous l’alias workspace `borsh_0_10`, distinct de Borsh 1.x utilisé par d’autres interfaces du workspace. Le dépôt officiel `metaplex-foundation/mpl-token-metadata` confirme le Program ID `metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s`. L’IDL officiel audité porte la version `1.14.0` et le blob `5df4a24f62c2743125be096cc174680790c92c18`; l’inventaire Rust généré des instructions porte le blob `3c42eec629f82e44ba690a7c4bd2177cc78f1d49`. -`kb_decoder_metadata_metaplex_token_metadata` implémente désormais `InstructionDecoder` pour `CreateMetadataAccountV3` et `UpdateMetadataAccountV2`. Les arguments sont lus avec les types Borsh officiels et projetés avec la feature `serde`; les payloads, chaînes, créateurs, frais, comptes et suffixes sont bornés. L’enregistrement dans le registre runtime reste différé jusqu’à validation de ce premier groupe. Les metadata Token-2022 incorporées, Metaplex Core, Bubblegum et le JSON externe restent des provenances ou composants distincts. +`kb_lib::decoder::metadata::metaplex_token_metadata` implémente désormais la couverture exhaustive des +58 discriminateurs `0..=57`. Les arguments sont lus avec les types Borsh officiels lorsque le SDK +les expose, ou avec un miroir local borné du wire publié pour les variantes historiques. Les +payloads, chaînes, créateurs, frais, comptes et suffixes sont bornés. + +Le même composant inventorie les 15 variantes `Key 0..=14` et décode les quatorze layouts +représentant un compte réel. Les parseurs utilisent `solana-pubkey ^4.2` pour vérifier owner, +PDA et bump, puis les comptes historiques reçoivent une politique de matérialisation historique +distincte de leur interdiction d’exécution. `Uninitialized` reste une sentinelle explicitement +refusée et toute variante future absente de l’inventaire fait échouer les tests. + +Les metadata Token‑2022 incorporées, Metaplex Core, Bubblegum et le JSON externe restent des +provenances ou composants distincts. Aucun fetch HTTP, IPFS ou Arweave n’appartient au replay +canonique de cette tranche. diff --git a/docs/TRACING_CONTRACT.md b/docs/TRACING_CONTRACT.md index 3022916..3c0d588 100644 --- a/docs/TRACING_CONTRACT.md +++ b/docs/TRACING_CONTRACT.md @@ -1,5 +1,5 @@ - + # Contrat de tracing par crate et composant @@ -57,6 +57,7 @@ Les targets déjà migrés vers les noms canoniques bot3 sont : ```text kb-lib.decoder.solana.core +kb-lib.decoder.metadata.metaplex_token_metadata kb-lib.decoder.spl.associated_token_account kb-lib.decoder.spl.elgamal_registry kb-lib.decoder.spl.memo diff --git a/kb-lib/Cargo.toml b/kb-lib/Cargo.toml index 6ce8264..b519a59 100644 --- a/kb-lib/Cargo.toml +++ b/kb-lib/Cargo.toml @@ -1,5 +1,5 @@ # file: kb-lib/Cargo.toml -# version: 7 +# version: 8 [package] name = "kb-lib" @@ -11,15 +11,18 @@ publish.workspace = true [dependencies] base64.workspace = true borsh.workspace = true +borsh_0_10.workspace = true bs58.workspace = true kb-core = { path = "../kb-core" } kb-program-ids = { path = "../kb-program-ids" } +mpl-token-metadata.workspace = true serde.workspace = true serde_json.workspace = true sha2.workspace = true solana-address-lookup-table-interface.workspace = true solana-compute-budget-interface.workspace = true solana-loader-v3-interface.workspace = true +solana-pubkey.workspace = true solana-sdk-ids.workspace = true solana-stake-interface.workspace = true solana-system-interface.workspace = true diff --git a/kb-lib/README.md b/kb-lib/README.md index 54f715a..52c73c5 100644 --- a/kb-lib/README.md +++ b/kb-lib/README.md @@ -1,5 +1,5 @@ - + # kb-lib @@ -80,6 +80,24 @@ est `docs/SPL_TOKEN_2022_MATRIX.json`. clé publique ElGamal et le wire complet. La matrice normative est `docs/SPL_ELGAMAL_REGISTRY_MATRIX.json`. +## Décodeur Metaplex Token Metadata + +`MetadataMetaplexTokenMetadataDecoder` couvre exclusivement +`metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s`. Il inventorie exactement les 58 +discriminateurs `0..=57`, conserve les arguments Borsh, les comptes positionnels, +les placeholders optionnels, les chemins outer/CPI et les transactions échouées +comme intentions non commitées. + +Le module public +`kb_lib::decoder::metadata::metaplex_token_metadata` expose aussi les parseurs +bornés des 15 variantes `Key 0..=14` : Metadata, Edition et Master Edition, +Edition Marker V1/V2, Token Record, records de délégation et d’autorité, Token +Owned Escrow et Reservation List historiques. Chaque parseur vérifie l’owner, +le PDA, le bump, la longueur et les suffixes avant projection. Les comptes +historiques gardent une provenance et un lifecycle matérialisables, mais une +politique d’exécution définitivement interdite. La matrice normative est +`docs/METAPLEX_TOKEN_METADATA_MATRIX.json`. + ## API publique utile - `SolanaCoreDecoder` : décodeur concret natif ; @@ -89,6 +107,8 @@ clé publique ElGamal et le wire complet. La matrice normative est - `SplToken2022Decoder` : décodeur exact des instructions Token‑2022 ; - `Token2022State`, `Token2022StateKind`, `Token2022TlvEntry` et `parse_token_2022_state()` : API publique de lecture d’état Token‑2022 ; - `SplElgamalRegistryDecoder`, `ElGamalRegistryState` et `parse_elgamal_registry_state()` : API publique du registre ElGamal ; +- `MetadataMetaplexTokenMetadataDecoder` : décodeur exact du programme Metaplex Token Metadata ; +- `kb_lib::decoder::metadata::metaplex_token_metadata` : parseurs et snapshots publics des comptes Metaplex ; - `InstructionDecoder` : contrat de reconnaissance, couverture et décodage contextualisé ; - `ProtocolDecoder` : contrat de compatibilité avec les observations historiques ; - `CoreInstructionReplayInput` : input source-neutral produit par l’extraction core ; diff --git a/kb-lib/src/decoder.rs b/kb-lib/src/decoder.rs index 9d9c3ae..f1d889a 100644 --- a/kb-lib/src/decoder.rs +++ b/kb-lib/src/decoder.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/decoder.rs -// version: 5 +// version: 6 //! Consolidated decoder modules. @@ -138,6 +138,8 @@ pub(crate) use self::spl::spl_token_entry_for_tag; /// Returns the first byte of one retained classic SPL Token payload. pub(crate) use self::spl::spl_token_payload_tag; +/// Exact decoder for the Metaplex Token Metadata program. +pub use self::metadata::MetadataMetaplexTokenMetadataDecoder; /// Exact byte length of one SPL ElGamal registry account. pub use self::spl::ELGAMAL_REGISTRY_ACCOUNT_LEN; /// Parsed SPL ElGamal public-key registry account. diff --git a/kb-lib/src/decoder/metadata.rs b/kb-lib/src/decoder/metadata.rs index 5d518c5..dfbd5af 100644 --- a/kb-lib/src/decoder/metadata.rs +++ b/kb-lib/src/decoder/metadata.rs @@ -1,7 +1,10 @@ // file: kb-lib/src/decoder/metadata.rs -// version: 1 +// version: 2 //! `metadata` decoder family. pub mod metaplex_token_metadata; pub mod spl_name_service; + +/// Exact decoder for the Metaplex Token Metadata program. +pub use self::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; diff --git a/kb-lib/src/decoder/metadata/metaplex_token_metadata.rs b/kb-lib/src/decoder/metadata/metaplex_token_metadata.rs index ce686ed..acdc764 100644 --- a/kb-lib/src/decoder/metadata/metaplex_token_metadata.rs +++ b/kb-lib/src/decoder/metadata/metaplex_token_metadata.rs @@ -1,10 +1,223 @@ // file: kb-lib/src/decoder/metadata/metaplex_token_metadata.rs -// version: 1 +// version: 2 -//! Migration boundary for legacy crate `kb_decoder_metadata_metaplex_token_metadata`. +//! Contextual instruction and bounded account decoder for Metaplex Token Metadata. -/// Legacy crate name retained for migration and compatibility tracking. -pub const LEGACY_CRATE: &str = "kb_decoder_metadata_metaplex_token_metadata"; +mod account; +mod canonical; +mod constants; +mod decoder; +mod instruction; -/// Current porting status. -pub const MIGRATION_STATUS: &str = "source-preserved-pending-port"; +/// Approve Collection Authority discriminator. +pub(crate) use self::constants::APPROVE_COLLECTION_AUTHORITY_DISCRIMINATOR; +/// Approve Use Authority discriminator. +pub(crate) use self::constants::APPROVE_USE_AUTHORITY_DISCRIMINATOR; +/// Bubblegum Set Collection Size discriminator. +pub(crate) use self::constants::BUBBLEGUM_SET_COLLECTION_SIZE_DISCRIMINATOR; +/// Burn discriminator. +pub(crate) use self::constants::BURN_DISCRIMINATOR; +/// Historical Burn Edition NFT discriminator. +pub(crate) use self::constants::BURN_EDITION_NFT_DISCRIMINATOR; +/// Historical Burn NFT discriminator. +pub(crate) use self::constants::BURN_NFT_DISCRIMINATOR; +/// Close Accounts discriminator. +pub(crate) use self::constants::CLOSE_ACCOUNTS_DISCRIMINATOR; +/// Close Escrow Account discriminator. +pub(crate) use self::constants::CLOSE_ESCROW_ACCOUNT_DISCRIMINATOR; +/// Collect discriminator. +pub(crate) use self::constants::COLLECT_DISCRIMINATOR; +/// Convert Master Edition V1 to V2 discriminator. +pub(crate) use self::constants::CONVERT_MASTER_EDITION_V1_TO_V2_DISCRIMINATOR; +/// Modern Create discriminator. +pub(crate) use self::constants::CREATE_DISCRIMINATOR; +/// Create Escrow Account discriminator. +pub(crate) use self::constants::CREATE_ESCROW_ACCOUNT_DISCRIMINATOR; +/// Historical Create Master Edition discriminator. +pub(crate) use self::constants::CREATE_MASTER_EDITION_DISCRIMINATOR; +/// Create Master Edition V3 discriminator. +pub(crate) use self::constants::CREATE_MASTER_EDITION_V3_DISCRIMINATOR; +/// Historical Create Metadata Account discriminator. +pub(crate) use self::constants::CREATE_METADATA_ACCOUNT_DISCRIMINATOR; +/// Historical Create Metadata Account V2 discriminator. +pub(crate) use self::constants::CREATE_METADATA_ACCOUNT_V2_DISCRIMINATOR; +/// Create Metadata Account V3 discriminator. +pub(crate) use self::constants::CREATE_METADATA_ACCOUNT_V3_DISCRIMINATOR; +/// Delegate discriminator. +pub(crate) use self::constants::DELEGATE_DISCRIMINATOR; +/// Deprecated Create Master Edition discriminator. +pub(crate) use self::constants::DEPRECATED_CREATE_MASTER_EDITION_DISCRIMINATOR; +/// Deprecated Create Reservation List discriminator. +pub(crate) use self::constants::DEPRECATED_CREATE_RESERVATION_LIST_DISCRIMINATOR; +/// Deprecated printing-token mint-new-edition discriminator. +pub(crate) use self::constants::DEPRECATED_MINT_NEW_EDITION_VIA_PRINTING_TOKEN_DISCRIMINATOR; +/// Deprecated Mint Printing Tokens discriminator. +pub(crate) use self::constants::DEPRECATED_MINT_PRINTING_TOKENS_DISCRIMINATOR; +/// Deprecated Mint Printing Tokens Via Token discriminator. +pub(crate) use self::constants::DEPRECATED_MINT_PRINTING_TOKENS_VIA_TOKEN_DISCRIMINATOR; +/// Deprecated Set Reservation List discriminator. +pub(crate) use self::constants::DEPRECATED_SET_RESERVATION_LIST_DISCRIMINATOR; +/// Number of bytes retained in malformed-wire diagnostics. +pub(crate) use self::constants::DIAGNOSTIC_PREFIX_BYTES; +/// Stable event payload contract version. +pub(crate) use self::constants::EVENT_VERSION; +/// Freeze Delegated Account discriminator. +pub(crate) use self::constants::FREEZE_DELEGATED_ACCOUNT_DISCRIMINATOR; +/// Current pre-release instruction inventory. +pub(crate) use self::constants::INSTRUCTION_ENTRIES; +/// Lock discriminator. +pub(crate) use self::constants::LOCK_DISCRIMINATOR; +/// Maximum encoded base64 instruction payload size. +pub(crate) use self::constants::MAX_BASE64_BYTES; +/// Maximum number of instruction accounts accepted by this decoder. +pub(crate) use self::constants::MAX_INSTRUCTION_ACCOUNTS; +/// Tracing target for this decoder. +/// Maximum retained instruction payload size. +pub(crate) use self::constants::MAX_INSTRUCTION_BYTES; +/// Migrate discriminator. +pub(crate) use self::constants::MIGRATE_DISCRIMINATOR; +/// Modern Mint discriminator. +pub(crate) use self::constants::MINT_DISCRIMINATOR; +/// Mint New Edition From Master Edition Via Token discriminator. +pub(crate) use self::constants::MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_TOKEN_DISCRIMINATOR; +/// Mint New Edition From Master Edition Via Vault Proxy discriminator. +pub(crate) use self::constants::MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_VAULT_PROXY_DISCRIMINATOR; +/// Modern Print discriminator. +pub(crate) use self::constants::PRINT_DISCRIMINATOR; +/// Puff Metadata discriminator. +pub(crate) use self::constants::PUFF_METADATA_DISCRIMINATOR; +/// Remove Creator Verification discriminator. +pub(crate) use self::constants::REMOVE_CREATOR_VERIFICATION_DISCRIMINATOR; +/// Resize discriminator. +pub(crate) use self::constants::RESIZE_DISCRIMINATOR; +/// Revoke Collection Authority discriminator. +pub(crate) use self::constants::REVOKE_COLLECTION_AUTHORITY_DISCRIMINATOR; +/// Revoke discriminator. +pub(crate) use self::constants::REVOKE_DISCRIMINATOR; +/// Revoke Use Authority discriminator. +pub(crate) use self::constants::REVOKE_USE_AUTHORITY_DISCRIMINATOR; +/// Set And Verify Collection discriminator. +pub(crate) use self::constants::SET_AND_VERIFY_COLLECTION_DISCRIMINATOR; +/// Set And Verify Sized Collection Item discriminator. +pub(crate) use self::constants::SET_AND_VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR; +/// Set Collection Size discriminator. +pub(crate) use self::constants::SET_COLLECTION_SIZE_DISCRIMINATOR; +/// Set Token Standard discriminator. +pub(crate) use self::constants::SET_TOKEN_STANDARD_DISCRIMINATOR; +/// Sign Metadata discriminator. +pub(crate) use self::constants::SIGN_METADATA_DISCRIMINATOR; +/// Stable decoder surface code. +pub(crate) use self::constants::SURFACE_CODE; +/// Thaw Delegated Account discriminator. +pub(crate) use self::constants::THAW_DELEGATED_ACCOUNT_DISCRIMINATOR; +/// Canonical tracing target for this crate. +pub(crate) use self::constants::TRACING_TARGET; +/// Transfer discriminator. +pub(crate) use self::constants::TRANSFER_DISCRIMINATOR; +/// Transfer Out Of Escrow discriminator. +pub(crate) use self::constants::TRANSFER_OUT_OF_ESCROW_DISCRIMINATOR; +/// Unlock discriminator. +pub(crate) use self::constants::UNLOCK_DISCRIMINATOR; +/// Unverify Collection discriminator. +pub(crate) use self::constants::UNVERIFY_COLLECTION_DISCRIMINATOR; +/// Modern Unverify discriminator. +pub(crate) use self::constants::UNVERIFY_DISCRIMINATOR; +/// Unverify Sized Collection Item discriminator. +pub(crate) use self::constants::UNVERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR; +/// Modern Update discriminator. +pub(crate) use self::constants::UPDATE_DISCRIMINATOR; +/// Historical Update Metadata Account discriminator. +pub(crate) use self::constants::UPDATE_METADATA_ACCOUNT_DISCRIMINATOR; +/// Update Metadata Account V2 discriminator. +pub(crate) use self::constants::UPDATE_METADATA_ACCOUNT_V2_DISCRIMINATOR; +/// Update Primary Sale Happened Via Token discriminator. +pub(crate) use self::constants::UPDATE_PRIMARY_SALE_HAPPENED_VIA_TOKEN_DISCRIMINATOR; +/// Modern Use discriminator. +pub(crate) use self::constants::USE_DISCRIMINATOR; +/// Utilize discriminator. +pub(crate) use self::constants::UTILIZE_DISCRIMINATOR; +/// Verify Collection discriminator. +pub(crate) use self::constants::VERIFY_COLLECTION_DISCRIMINATOR; +/// Modern Verify discriminator. +pub(crate) use self::constants::VERIFY_DISCRIMINATOR; +/// Verify Sized Collection Item discriminator. +pub(crate) use self::constants::VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR; +/// Crate-root access to `ParsedInstruction` from `instruction`. +pub(crate) use self::instruction::ParsedInstruction; +/// Crate-root access to `decode` from `instruction`. +pub(crate) use self::instruction::decode; +/// Crate-root access to `entry_for_discriminator` from `instruction`. +pub(crate) use self::instruction::entry_for_discriminator; +/// Crate-root access to `payload_discriminator` from `instruction`. +pub(crate) use self::instruction::payload_discriminator; + +/// Canonical projection shared by Collection and Use Authority records. +pub use self::account::AuthorityRecordAccountSnapshot; +/// Canonical projection shared by Metadata and Holder delegate records. +pub use self::account::DelegateRecordAccountSnapshot; +/// Supported edition account layout. +pub use self::account::EditionAccountKind; +/// Canonical bounded projection of one edition or master-edition account. +pub use self::account::EditionAccountSnapshot; +/// Supported Edition Marker account layout. +pub use self::account::EditionMarkerAccountKind; +/// Canonical bounded projection of one Edition Marker account. +pub use self::account::EditionMarkerAccountSnapshot; +/// Exhaustive published Metaplex account-key inventory. +pub use self::account::METADATA_ACCOUNT_LAYOUT_AUDIT; +/// Bounded error returned while decoding a Metadata account. +pub use self::account::MetadataAccountDecodeError; +/// One entry in the exhaustive published account-key audit. +pub use self::account::MetadataAccountLayoutAuditEntry; +/// Canonical bounded projection of one Metadata account. +pub use self::account::MetadataAccountSnapshot; +/// One bounded historical reservation entry. +pub use self::account::ReservationEntrySnapshot; +/// Historical Reservation List account generation. +pub use self::account::ReservationListAccountKind; +/// Canonical decode-only projection of a historical Reservation List account. +pub use self::account::ReservationListAccountSnapshot; +/// Canonical projection of one Token Owned Escrow account. +pub use self::account::TokenOwnedEscrowAccountSnapshot; +/// Authority form used by one Token Owned Escrow account. +pub use self::account::TokenOwnedEscrowAuthorityKind; +/// Canonical bounded projection of one programmable Token Record account. +pub use self::account::TokenRecordAccountSnapshot; +/// Decodes one Collection Authority Record account. +pub use self::account::decode_collection_authority_record_account; +/// Decodes and validates one edition or master-edition account snapshot. +pub use self::account::decode_edition_account; +/// Decodes and validates one Edition Marker account snapshot. +pub use self::account::decode_edition_marker_account; +/// Decodes one Holder Delegate Record account. +pub use self::account::decode_holder_delegate_record_account; +/// Decodes and validates one canonical Metadata account snapshot. +pub use self::account::decode_metadata_account; +/// Decodes one Metadata Delegate Record account. +pub use self::account::decode_metadata_delegate_record_account; +/// Decodes one historical Reservation List V1 or V2 account. +pub use self::account::decode_reservation_list_account; +/// Decodes one Token Owned Escrow account. +pub use self::account::decode_token_owned_escrow_account; +/// Decodes and validates one programmable Token Record account snapshot. +pub use self::account::decode_token_record_account; +/// Decodes one Use Authority Record account. +pub use self::account::decode_use_authority_record_account; +/// Stable identity and policy of one canonical account snapshot. +pub use self::canonical::CanonicalAccountIdentity; +/// Lifecycle class of one canonical account layout. +pub use self::canonical::CanonicalAccountLifecycle; +/// Provenance retained for one canonical account observation. +pub use self::canonical::CanonicalAccountProvenance; +/// Execution policy separated from historical materialization. +pub use self::canonical::CanonicalExecutionPolicy; +/// Materialization policy for active or historical state. +pub use self::canonical::CanonicalMaterializationPolicy; +/// Canonical account envelope used by materializers and replay. +pub use self::canonical::CanonicalMetadataAccountSnapshot; +/// Typed canonical Metaplex account state. +pub use self::canonical::CanonicalMetadataAccountState; +/// Origin of one canonical account observation. +pub use self::canonical::CanonicalObservationSource; +/// Exact Metaplex Token Metadata decoder. +pub use self::decoder::MetadataMetaplexTokenMetadataDecoder; diff --git a/kb-lib/src/decoder/metadata/metaplex_token_metadata/account.rs b/kb-lib/src/decoder/metadata/metaplex_token_metadata/account.rs new file mode 100644 index 0000000..ffc6854 --- /dev/null +++ b/kb-lib/src/decoder/metadata/metaplex_token_metadata/account.rs @@ -0,0 +1,2631 @@ +// file: kb-lib/src/decoder/metadata/metaplex_token_metadata/account.rs +// version: 11 + +//! Bounded decoding and PDA validation for Metaplex Token Metadata accounts. + +const MAX_METADATA_ACCOUNT_BYTES: usize = 1_024; +const MAX_NAME_BYTES: usize = 32; +const MAX_SYMBOL_BYTES: usize = 10; +const MAX_URI_BYTES: usize = 200; +const MAX_CREATORS: usize = 5; +const EDITION_MARKER_V1_BITS: u64 = 248; +const MAX_EDITION_MARKER_V2_LEDGER_BYTES: usize = 1_048_576; + +/// Bounded error returned while decoding a Metadata account. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum MetadataAccountDecodeError { + /// The account key is not a valid Solana public key. + InvalidAccountKey, + /// The account owner is not a valid Solana public key. + InvalidOwner, + /// The account is owned by another program. + ForeignOwner, + /// The account data is empty or exceeds the explicit bound. + InvalidDataLength, + /// The account payload is not a supported Metadata layout. + InvalidMetadataLayout, + /// One bounded string or vector exceeds the official maximum. + MetadataBoundsExceeded, + /// The supplied account key is not the canonical Metadata PDA for the parsed mint. + InvalidMetadataPda, + /// The parsed account key discriminant is not Metadata. + InvalidMetadataKey, + /// The supplied account key is not the canonical Edition Marker PDA. + InvalidEditionMarkerPda, + /// The supplied account key is not the canonical Token Record PDA. + InvalidTokenRecordPda, + /// The parsed account key discriminant is not Token Record. + InvalidTokenRecordKey, + /// The supplied account key is not the canonical delegate or authority record PDA. + InvalidAuthorityRecordPda, + /// The supplied account key is not the canonical Token Owned Escrow PDA. + InvalidTokenOwnedEscrowPda, + /// The parsed account key discriminant is not Token Owned Escrow. + InvalidTokenOwnedEscrowKey, + /// The parsed account key discriminant is not the expected delegate or authority record. + InvalidAuthorityRecordKey, + /// The supplied account key is not the canonical historical Reservation List PDA. + InvalidReservationListPda, + /// The parsed account key discriminant is not Reservation List V1 or V2. + InvalidReservationListKey, + /// The historical reservation vector or padded payload exceeds official bounds. + ReservationListBoundsExceeded, + /// The Edition Marker ledger exceeds the explicit decoder bound. + EditionMarkerBoundsExceeded, + /// The parsed account could not be projected to bounded JSON. + ProjectionFailed, +} + +impl std::fmt::Display for MetadataAccountDecodeError { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = match self { + Self::InvalidAccountKey => "metadata account key is invalid", + Self::InvalidOwner => "metadata account owner is invalid", + Self::ForeignOwner => "metadata account is owned by another program", + Self::InvalidDataLength => "metadata account data length is invalid", + Self::InvalidMetadataLayout => "metadata account layout is invalid", + Self::MetadataBoundsExceeded => "metadata account exceeds bounded fields", + Self::InvalidMetadataPda => "metadata account PDA is invalid", + Self::InvalidMetadataKey => "metadata account key discriminant is invalid", + Self::InvalidEditionMarkerPda => "edition marker account PDA is invalid", + Self::InvalidTokenRecordPda => "token record account PDA is invalid", + Self::InvalidTokenRecordKey => "token record account key discriminant is invalid", + Self::InvalidAuthorityRecordPda => "delegate or authority record PDA is invalid", + Self::InvalidTokenOwnedEscrowPda => "token owned escrow PDA is invalid", + Self::InvalidTokenOwnedEscrowKey => "token owned escrow key discriminant is invalid", + Self::InvalidAuthorityRecordKey => { + "delegate or authority record key discriminant is invalid" + }, + Self::InvalidReservationListPda => "reservation list account PDA is invalid", + Self::InvalidReservationListKey => { + "reservation list account key discriminant is invalid" + }, + Self::ReservationListBoundsExceeded => { + "reservation list account exceeds official bounds" + }, + Self::EditionMarkerBoundsExceeded => "edition marker ledger exceeds bounded fields", + Self::ProjectionFailed => "metadata account JSON projection failed", + }; + return formatter.write_str(message); + } +} + +impl std::error::Error for MetadataAccountDecodeError {} + +/// One audited Metaplex account-key layout. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MetadataAccountLayoutAuditEntry { + /// Published `Key` variant name. + pub key_name: &'static str, + /// Exact Borsh discriminant byte. + pub discriminant: u8, + /// Decoder entry point or explicit rejection policy. + pub decoder: &'static str, + /// Lifecycle classification used by later materialization and execution layers. + pub lifecycle: &'static str, +} + +/// Exhaustive inventory of every published Metaplex Token Metadata `Key` variant. +pub const METADATA_ACCOUNT_LAYOUT_AUDIT: &[MetadataAccountLayoutAuditEntry] = &[ + MetadataAccountLayoutAuditEntry { + key_name: "Uninitialized", + discriminant: 0, + decoder: "reject_uninitialized", + lifecycle: "sentinel", + }, + MetadataAccountLayoutAuditEntry { + key_name: "EditionV1", + discriminant: 1, + decoder: "decode_edition_account", + lifecycle: "active", + }, + MetadataAccountLayoutAuditEntry { + key_name: "MasterEditionV1", + discriminant: 2, + decoder: "decode_edition_account", + lifecycle: "historical_materializable", + }, + MetadataAccountLayoutAuditEntry { + key_name: "ReservationListV1", + discriminant: 3, + decoder: "decode_reservation_list_account", + lifecycle: "historical_materializable", + }, + MetadataAccountLayoutAuditEntry { + key_name: "MetadataV1", + discriminant: 4, + decoder: "decode_metadata_account", + lifecycle: "active", + }, + MetadataAccountLayoutAuditEntry { + key_name: "ReservationListV2", + discriminant: 5, + decoder: "decode_reservation_list_account", + lifecycle: "historical_materializable", + }, + MetadataAccountLayoutAuditEntry { + key_name: "MasterEditionV2", + discriminant: 6, + decoder: "decode_edition_account", + lifecycle: "active", + }, + MetadataAccountLayoutAuditEntry { + key_name: "EditionMarker", + discriminant: 7, + decoder: "decode_edition_marker_account", + lifecycle: "active", + }, + MetadataAccountLayoutAuditEntry { + key_name: "UseAuthorityRecord", + discriminant: 8, + decoder: "decode_use_authority_record_account", + lifecycle: "active", + }, + MetadataAccountLayoutAuditEntry { + key_name: "CollectionAuthorityRecord", + discriminant: 9, + decoder: "decode_collection_authority_record_account", + lifecycle: "active", + }, + MetadataAccountLayoutAuditEntry { + key_name: "TokenOwnedEscrow", + discriminant: 10, + decoder: "decode_token_owned_escrow_account", + lifecycle: "active", + }, + MetadataAccountLayoutAuditEntry { + key_name: "TokenRecord", + discriminant: 11, + decoder: "decode_token_record_account", + lifecycle: "active", + }, + MetadataAccountLayoutAuditEntry { + key_name: "MetadataDelegate", + discriminant: 12, + decoder: "decode_metadata_delegate_record_account", + lifecycle: "active", + }, + MetadataAccountLayoutAuditEntry { + key_name: "EditionMarkerV2", + discriminant: 13, + decoder: "decode_edition_marker_account", + lifecycle: "active_experimental", + }, + MetadataAccountLayoutAuditEntry { + key_name: "HolderDelegate", + discriminant: 14, + decoder: "decode_holder_delegate_record_account", + lifecycle: "active", + }, +]; + +/// Canonical bounded projection of one Metadata account. +#[derive(Clone, Debug, PartialEq)] +pub struct MetadataAccountSnapshot { + /// Canonical account address. + pub account: String, + /// Canonical Metadata PDA bump. + pub bump: u8, + /// Mint carried by the account. + pub mint: String, + /// Current update authority. + pub update_authority: String, + /// Trimmed metadata name. + pub name: String, + /// Trimmed metadata symbol. + pub symbol: String, + /// Trimmed metadata URI. + pub uri: String, + /// Seller fee in basis points. + pub seller_fee_basis_points: u16, + /// Whether primary sale happened. + pub primary_sale_happened: bool, + /// Whether the metadata is mutable. + pub is_mutable: bool, + /// Complete bounded account projection. + pub payload_json: serde_json::Value, +} + +/// Decodes one Metadata account after validating owner, bounds and canonical PDA. +pub fn decode_metadata_account( + account: &str, + owner: &str, + data: &[u8], +) -> std::result::Result { + let account_key = match ::from_str(account) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let owner_key = match ::from_str(owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + let expected_owner = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + if owner_key != expected_owner { + return std::result::Result::Err(MetadataAccountDecodeError::ForeignOwner); + } + if data.is_empty() || data.len() > MAX_METADATA_ACCOUNT_BYTES { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let metadata = match mpl_token_metadata::accounts::Metadata::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + if metadata.key != mpl_token_metadata::types::Key::MetadataV1 { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataKey); + } + let name = metadata.name.trim_end_matches('\0').to_string(); + let symbol = metadata.symbol.trim_end_matches('\0').to_string(); + let uri = metadata.uri.trim_end_matches('\0').to_string(); + if name.len() > MAX_NAME_BYTES || symbol.len() > MAX_SYMBOL_BYTES || uri.len() > MAX_URI_BYTES { + return std::result::Result::Err(MetadataAccountDecodeError::MetadataBoundsExceeded); + } + if metadata + .creators + .as_ref() + .is_some_and(|creators| return creators.len() > MAX_CREATORS) + { + return std::result::Result::Err(MetadataAccountDecodeError::MetadataBoundsExceeded); + } + let (expected_pda, bump) = mpl_token_metadata::accounts::Metadata::find_pda(&metadata.mint); + if account_key.to_string() != expected_pda.to_string() { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataPda); + } + let payload_json = match serde_json::to_value(&metadata) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(MetadataAccountSnapshot { + account: account_key.to_string(), + bump, + mint: metadata.mint.to_string(), + update_authority: metadata.update_authority.to_string(), + name, + symbol, + uri, + seller_fee_basis_points: metadata.seller_fee_basis_points, + primary_sale_happened: metadata.primary_sale_happened, + is_mutable: metadata.is_mutable, + payload_json, + }); +} + +/// Supported edition account layout. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum EditionAccountKind { + /// Historical Master Edition V1 layout. + MasterEditionV1, + /// Current Master Edition V2 layout. + MasterEditionV2, + /// Printed Edition V1 layout. + EditionV1, +} + +/// Canonical bounded projection of one edition or master-edition account. +#[derive(Clone, Debug, PartialEq)] +pub struct EditionAccountSnapshot { + /// Canonical account address. + pub account: String, + /// Canonical edition PDA bump. + pub bump: u8, + /// Mint used to derive the edition PDA. + pub mint: String, + /// Decoded account layout. + pub kind: EditionAccountKind, + /// Current printed supply for a master edition. + pub supply: std::option::Option, + /// Optional maximum supply for a master edition. + pub max_supply: std::option::Option, + /// Parent master edition for a printed edition. + pub parent: std::option::Option, + /// Printed edition number. + pub edition: std::option::Option, + /// Historical printing mint carried by Master Edition V1. + pub printing_mint: std::option::Option, + /// Historical one-time printing authorization mint carried by Master Edition V1. + pub one_time_printing_authorization_mint: std::option::Option, + /// Complete bounded account projection. + pub payload_json: serde_json::Value, +} + +/// Decodes one MasterEditionV1, MasterEditionV2 or Edition account with exact owner and PDA validation. +pub fn decode_edition_account( + account: &str, + owner: &str, + mint: &str, + data: &[u8], +) -> std::result::Result { + let account_key = match ::from_str(account) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let owner_key = match ::from_str(owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + let expected_owner = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + if owner_key != expected_owner { + return std::result::Result::Err(MetadataAccountDecodeError::ForeignOwner); + } + if data.is_empty() || data.len() > 128 { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let (expected_pda, bump) = match mint.parse() { + std::result::Result::Ok(mint_key) => { + mpl_token_metadata::accounts::MasterEdition::find_pda(&mint_key) + }, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + if account_key.to_string() != expected_pda.to_string() { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataPda); + } + let key = data[0]; + if key == mpl_token_metadata::types::Key::MasterEditionV1 as u8 { + let value = match mpl_token_metadata::accounts::DeprecatedMasterEditionV1::from_bytes(data) + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(EditionAccountSnapshot { + account: account_key.to_string(), + bump, + mint: mint.to_string(), + kind: EditionAccountKind::MasterEditionV1, + supply: std::option::Option::Some(value.supply), + max_supply: value.max_supply, + parent: std::option::Option::None, + edition: std::option::Option::None, + printing_mint: std::option::Option::Some(value.printing_mint.to_string()), + one_time_printing_authorization_mint: std::option::Option::Some( + value.one_time_printing_authorization_mint.to_string(), + ), + payload_json, + }); + } + if key == mpl_token_metadata::types::Key::MasterEditionV2 as u8 { + let value = match mpl_token_metadata::accounts::MasterEdition::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(EditionAccountSnapshot { + account: account_key.to_string(), + bump, + mint: mint.to_string(), + kind: EditionAccountKind::MasterEditionV2, + supply: std::option::Option::Some(value.supply), + max_supply: value.max_supply, + parent: std::option::Option::None, + edition: std::option::Option::None, + printing_mint: std::option::Option::None, + one_time_printing_authorization_mint: std::option::Option::None, + payload_json, + }); + } + if key == mpl_token_metadata::types::Key::EditionV1 as u8 { + if data.len() != mpl_token_metadata::accounts::Edition::LEN { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let value = match mpl_token_metadata::accounts::Edition::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(EditionAccountSnapshot { + account: account_key.to_string(), + bump, + mint: mint.to_string(), + kind: EditionAccountKind::EditionV1, + supply: std::option::Option::None, + max_supply: std::option::Option::None, + parent: std::option::Option::Some(value.parent.to_string()), + edition: std::option::Option::Some(value.edition), + printing_mint: std::option::Option::None, + one_time_printing_authorization_mint: std::option::Option::None, + payload_json, + }); + } + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataKey); +} + +/// Supported Edition Marker account layout. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum EditionMarkerAccountKind { + /// Fixed 248-edition historical/current marker layout. + EditionMarkerV1, + /// Extensible marker layout with one PDA per mint. + EditionMarkerV2, +} + +/// Canonical bounded projection of one Edition Marker account. +#[derive(Clone, Debug, PartialEq)] +pub struct EditionMarkerAccountSnapshot { + /// Canonical account address. + pub account: String, + /// Canonical PDA bump. + pub bump: u8, + /// Mint used to derive the marker PDA. + pub mint: String, + /// Decoded marker layout. + pub kind: EditionMarkerAccountKind, + /// V1 group number (`edition / 248`), absent for V2. + pub marker_group: std::option::Option, + /// Requested edition used to inspect the bitmap. + pub edition: u64, + /// Byte index inside the ledger. + pub byte_index: usize, + /// Big-endian bit mask inside the selected byte. + pub bit_mask: u8, + /// Whether the requested edition is already marked. + pub edition_taken: bool, + /// Exact bounded ledger bytes. + pub ledger: std::vec::Vec, + /// Complete bounded account projection. + pub payload_json: serde_json::Value, +} + +/// Decodes EditionMarker or EditionMarkerV2 with exact owner, bounds, bitmap and PDA validation. +pub fn decode_edition_marker_account( + account: &str, + owner: &str, + mint: &str, + edition: u64, + data: &[u8], +) -> std::result::Result { + let account_key = match ::from_str(account) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let owner_key = match ::from_str(owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + let program_key = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + if owner_key != program_key { + return std::result::Result::Err(MetadataAccountDecodeError::ForeignOwner); + } + let mint_key = match ::from_str(mint) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + if data.is_empty() { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + if data[0] == mpl_token_metadata::types::Key::EditionMarker as u8 { + if data.len() != mpl_token_metadata::accounts::EditionMarker::LEN { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let marker_group = edition / EDITION_MARKER_V1_BITS; + let marker_seed = marker_group.to_string(); + let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program_key.as_ref(), + mint_key.as_ref(), + b"edition", + marker_seed.as_bytes(), + ], + &program_key, + ); + if account_key != expected_pda { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidEditionMarkerPda); + } + let value = match mpl_token_metadata::accounts::EditionMarker::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let offset = (edition % EDITION_MARKER_V1_BITS) as usize; + let byte_index = offset / 8; + let bit_mask = 1u8 << (7 - (offset % 8)); + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(EditionMarkerAccountSnapshot { + account: account_key.to_string(), + bump, + mint: mint_key.to_string(), + kind: EditionMarkerAccountKind::EditionMarkerV1, + marker_group: std::option::Option::Some(marker_group), + edition, + byte_index, + bit_mask, + edition_taken: value.ledger[byte_index] & bit_mask != 0, + ledger: value.ledger.to_vec(), + payload_json, + }); + } + if data[0] == mpl_token_metadata::types::Key::EditionMarkerV2 as u8 { + if data.len() < 5 || data.len() > MAX_EDITION_MARKER_V2_LEDGER_BYTES + 5 { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address( + &[b"metadata", program_key.as_ref(), mint_key.as_ref(), b"edition", b"marker"], + &program_key, + ); + if account_key != expected_pda { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidEditionMarkerPda); + } + let value = match mpl_token_metadata::accounts::EditionMarkerV2::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + if value.ledger.len() > MAX_EDITION_MARKER_V2_LEDGER_BYTES { + return std::result::Result::Err( + MetadataAccountDecodeError::EditionMarkerBoundsExceeded, + ); + } + if data.len() != value.ledger.len() + 5 { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let byte_index_u64 = edition / 8; + let byte_index = match usize::try_from(byte_index_u64) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err( + MetadataAccountDecodeError::EditionMarkerBoundsExceeded, + ); + }, + }; + let bit_mask = 1u8 << (7 - (edition % 8) as usize); + let edition_taken = + value.ledger.get(byte_index).is_some_and(|entry| return entry & bit_mask != 0); + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(EditionMarkerAccountSnapshot { + account: account_key.to_string(), + bump, + mint: mint_key.to_string(), + kind: EditionMarkerAccountKind::EditionMarkerV2, + marker_group: std::option::Option::None, + edition, + byte_index, + bit_mask, + edition_taken, + ledger: value.ledger, + payload_json, + }); + } + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataKey); +} + +/// Canonical bounded projection of one programmable Token Record account. +#[derive(Clone, Debug, PartialEq)] +pub struct TokenRecordAccountSnapshot { + /// Canonical Token Record account address. + pub account: String, + /// Canonical Token Record PDA bump computed from mint and token account. + pub bump: u8, + /// Mint used to derive the Token Record PDA. + pub mint: String, + /// Token account used to derive the Token Record PDA. + pub token: String, + /// Bump stored by the on-chain Token Record layout. + pub stored_bump: u8, + /// Current programmable token state. + pub state: String, + /// Optional Token Authorization Rules revision. + pub rule_set_revision: std::option::Option, + /// Optional active delegate. + pub delegate: std::option::Option, + /// Optional active delegate role. + pub delegate_role: std::option::Option, + /// Optional locked-transfer destination. + pub locked_transfer: std::option::Option, + /// Whether the token is currently locked. + pub locked: bool, + /// Complete bounded account projection. + pub payload_json: serde_json::Value, +} + +/// Decodes one Token Record account after exact owner, length, discriminant and PDA validation. +pub fn decode_token_record_account( + account: &str, + owner: &str, + mint: &str, + token: &str, + data: &[u8], +) -> std::result::Result { + let account_key = match ::from_str(account) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let owner_key = match ::from_str(owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + let expected_owner = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + if owner_key != expected_owner { + return std::result::Result::Err(MetadataAccountDecodeError::ForeignOwner); + } + if data.len() != mpl_token_metadata::accounts::TokenRecord::LEN { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let mint_key = match ::from_str(mint) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let token_key = match ::from_str(token) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + expected_owner.as_ref(), + mint_key.as_ref(), + b"token_record", + token_key.as_ref(), + ], + &expected_owner, + ); + if account_key != expected_pda { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidTokenRecordPda); + } + let value = match mpl_token_metadata::accounts::TokenRecord::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + if value.key != mpl_token_metadata::types::Key::TokenRecord { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidTokenRecordKey); + } + if value.bump != bump { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidTokenRecordPda); + } + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + let state = format!("{:?}", value.state); + let delegate_role = value.delegate_role.as_ref().map(|role| return format!("{role:?}")); + let locked = value.state == mpl_token_metadata::types::TokenState::Locked; + return std::result::Result::Ok(TokenRecordAccountSnapshot { + account: account_key.to_string(), + bump, + mint: mint_key.to_string(), + token: token_key.to_string(), + stored_bump: value.bump, + state, + rule_set_revision: value.rule_set_revision, + delegate: value.delegate.map(|delegate| return delegate.to_string()), + delegate_role, + locked_transfer: value + .locked_transfer + .map(|locked_transfer| return locked_transfer.to_string()), + locked, + payload_json, + }); +} + +/// Canonical projection shared by Metadata and Holder delegate records. +#[derive(Clone, Debug, PartialEq)] +pub struct DelegateRecordAccountSnapshot { + /// Canonical delegate-record account address. + pub account: String, + /// Canonical PDA bump. + pub bump: u8, + /// Delegate-record family. + pub kind: String, + /// Mint carried by the record. + pub mint: String, + /// Role seed used by the PDA. + pub role: String, + /// Authority or holder used by the PDA. + pub authority: String, + /// Delegate carried by the record. + pub delegate: String, + /// Update authority stored by the record. + pub update_authority: String, + /// Bump stored by the account. + pub stored_bump: u8, + /// Complete bounded account projection. + pub payload_json: serde_json::Value, +} + +fn parse_record_boundary( + account: &str, + owner: &str, +) -> std::result::Result<(solana_pubkey::Pubkey, solana_pubkey::Pubkey), MetadataAccountDecodeError> +{ + let account_key = match ::from_str(account) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let owner_key = match ::from_str(owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + let expected_owner = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + if owner_key != expected_owner { + return std::result::Result::Err(MetadataAccountDecodeError::ForeignOwner); + } + return std::result::Result::Ok((account_key, expected_owner)); +} + +/// Decodes a Metadata Delegate Record with exact role, authority, delegate and bump validation. +pub fn decode_metadata_delegate_record_account( + account: &str, + owner: &str, + role: mpl_token_metadata::types::MetadataDelegateRole, + data: &[u8], +) -> std::result::Result { + let (account_key, program) = match parse_record_boundary(account, owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if data.len() != mpl_token_metadata::accounts::MetadataDelegateRecord::LEN { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let value = match mpl_token_metadata::accounts::MetadataDelegateRecord::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + if value.key != mpl_token_metadata::types::Key::MetadataDelegate { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAuthorityRecordKey); + } + let role_seed = role.to_string(); + let mint = match ::from_str(&value.mint.to_string()) + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let authority = match ::from_str( + &value.update_authority.to_string(), + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let delegate = + match ::from_str(&value.delegate.to_string()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + mint.as_ref(), + role_seed.as_bytes(), + authority.as_ref(), + delegate.as_ref(), + ], + &program, + ); + if account_key != expected_pda || value.bump != bump { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAuthorityRecordPda); + } + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(DelegateRecordAccountSnapshot { + account: account_key.to_string(), + bump, + kind: "MetadataDelegateRecord".to_string(), + mint: mint.to_string(), + role: role_seed, + authority: authority.to_string(), + delegate: delegate.to_string(), + update_authority: value.update_authority.to_string(), + stored_bump: value.bump, + payload_json, + }); +} + +/// Decodes a Holder Delegate Record with exact role, holder, delegate and bump validation. +pub fn decode_holder_delegate_record_account( + account: &str, + owner: &str, + holder: &str, + role: mpl_token_metadata::types::HolderDelegateRole, + data: &[u8], +) -> std::result::Result { + let (account_key, program) = match parse_record_boundary(account, owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if data.len() != mpl_token_metadata::accounts::HolderDelegateRecord::LEN { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let value = match mpl_token_metadata::accounts::HolderDelegateRecord::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + if value.key != mpl_token_metadata::types::Key::HolderDelegate { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAuthorityRecordKey); + } + let holder_key = match ::from_str(holder) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let role_seed = role.to_string(); + let mint = match ::from_str(&value.mint.to_string()) + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let delegate = + match ::from_str(&value.delegate.to_string()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + mint.as_ref(), + role_seed.as_bytes(), + holder_key.as_ref(), + delegate.as_ref(), + ], + &program, + ); + if account_key != expected_pda || value.bump != bump { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAuthorityRecordPda); + } + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(DelegateRecordAccountSnapshot { + account: account_key.to_string(), + bump, + kind: "HolderDelegateRecord".to_string(), + mint: mint.to_string(), + role: role_seed, + authority: holder_key.to_string(), + delegate: delegate.to_string(), + update_authority: value.update_authority.to_string(), + stored_bump: value.bump, + payload_json, + }); +} + +/// Canonical projection of Collection Authority and Use Authority records. +#[derive(Clone, Debug, PartialEq)] +pub struct AuthorityRecordAccountSnapshot { + /// Canonical record account address. + pub account: String, + /// Canonical PDA bump. + pub bump: u8, + /// Record family. + pub kind: String, + /// Mint used by the PDA. + pub mint: String, + /// Authority used by the PDA. + pub authority: String, + /// Optional update authority stored by Collection Authority Record. + pub update_authority: std::option::Option, + /// Optional remaining use allowance stored by Use Authority Record. + pub allowed_uses: std::option::Option, + /// Bump stored by the account. + pub stored_bump: u8, + /// Complete bounded account projection. + pub payload_json: serde_json::Value, +} + +/// Decodes a Collection Authority Record with exact PDA and bump validation. +pub fn decode_collection_authority_record_account( + account: &str, + owner: &str, + mint: &str, + authority: &str, + data: &[u8], +) -> std::result::Result { + let (account_key, program) = match parse_record_boundary(account, owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if data.len() != 3 && data.len() != 35 { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let mint_key = match ::from_str(mint) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let authority_key = match ::from_str(authority) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + mint_key.as_ref(), + b"collection_authority", + authority_key.as_ref(), + ], + &program, + ); + if account_key != expected_pda { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAuthorityRecordPda); + } + let value = match mpl_token_metadata::accounts::CollectionAuthorityRecord::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + if value.key != mpl_token_metadata::types::Key::CollectionAuthorityRecord { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAuthorityRecordKey); + } + if value.bump != bump { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAuthorityRecordPda); + } + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(AuthorityRecordAccountSnapshot { + account: account_key.to_string(), + bump, + kind: "CollectionAuthorityRecord".to_string(), + mint: mint_key.to_string(), + authority: authority_key.to_string(), + update_authority: value.update_authority.map(|key| return key.to_string()), + allowed_uses: std::option::Option::None, + stored_bump: value.bump, + payload_json, + }); +} + +/// Decodes a Use Authority Record with exact length, PDA and bump validation. +pub fn decode_use_authority_record_account( + account: &str, + owner: &str, + mint: &str, + authority: &str, + data: &[u8], +) -> std::result::Result { + let (account_key, program) = match parse_record_boundary(account, owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if data.len() != mpl_token_metadata::accounts::UseAuthorityRecord::LEN { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let mint_key = match ::from_str(mint) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let authority_key = match ::from_str(authority) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + mint_key.as_ref(), + b"user", + authority_key.as_ref(), + ], + &program, + ); + if account_key != expected_pda { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAuthorityRecordPda); + } + let value = match mpl_token_metadata::accounts::UseAuthorityRecord::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + if value.key != mpl_token_metadata::types::Key::UseAuthorityRecord { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAuthorityRecordKey); + } + if value.bump != bump { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAuthorityRecordPda); + } + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(AuthorityRecordAccountSnapshot { + account: account_key.to_string(), + bump, + kind: "UseAuthorityRecord".to_string(), + mint: mint_key.to_string(), + authority: authority_key.to_string(), + update_authority: std::option::Option::None, + allowed_uses: std::option::Option::Some(value.allowed_uses), + stored_bump: value.bump, + payload_json, + }); +} + +/// Authority form used by one Token Owned Escrow account. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum TokenOwnedEscrowAuthorityKind { + /// Escrow controlled by the owner of the base token. + TokenOwner, + /// Escrow controlled by an explicit creator key. + Creator, +} + +/// Canonical bounded projection of one Token Owned Escrow account. +#[derive(Clone, Debug, PartialEq)] +pub struct TokenOwnedEscrowAccountSnapshot { + /// Canonical escrow account address. + pub account: String, + /// Canonical PDA bump. + pub bump: u8, + /// Base token mint stored by the account. + pub base_token: String, + /// Authority form encoded by the account. + pub authority_kind: TokenOwnedEscrowAuthorityKind, + /// Optional creator authority. + pub creator: std::option::Option, + /// Bump stored by the account. + pub stored_bump: u8, + /// Complete bounded account projection. + pub payload_json: serde_json::Value, +} + +/// Decodes one Token Owned Escrow account with exact owner, layout, authority seeds and bump validation. +pub fn decode_token_owned_escrow_account( + account: &str, + owner: &str, + data: &[u8], +) -> std::result::Result { + let (account_key, program) = match parse_record_boundary(account, owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + if data.len() != 35 && data.len() != 67 { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + } + let value = match mpl_token_metadata::accounts::TokenOwnedEscrow::from_bytes(data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + if value.key != mpl_token_metadata::types::Key::TokenOwnedEscrow { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidTokenOwnedEscrowKey); + } + let base_token = + match ::from_str(&value.base_token.to_string()) + { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let (authority_kind, creator, expected_pda, bump) = match &value.authority { + mpl_token_metadata::types::EscrowAuthority::TokenOwner => { + let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address( + &[b"metadata", program.as_ref(), base_token.as_ref(), &[0], b"escrow"], + &program, + ); + ( + TokenOwnedEscrowAuthorityKind::TokenOwner, + std::option::Option::None, + expected_pda, + bump, + ) + }, + mpl_token_metadata::types::EscrowAuthority::Creator(creator_key) => { + let creator_text = creator_key.to_string(); + let creator_key = + match ::from_str(&creator_text) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err( + MetadataAccountDecodeError::InvalidAccountKey, + ); + }, + }; + let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + base_token.as_ref(), + &[1], + creator_key.as_ref(), + b"escrow", + ], + &program, + ); + ( + TokenOwnedEscrowAuthorityKind::Creator, + std::option::Option::Some(creator_text), + expected_pda, + bump, + ) + }, + }; + if account_key != expected_pda || value.bump != bump { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidTokenOwnedEscrowPda); + } + let payload_json = match serde_json::to_value(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::ProjectionFailed); + }, + }; + return std::result::Result::Ok(TokenOwnedEscrowAccountSnapshot { + account: account_key.to_string(), + bump, + base_token: value.base_token.to_string(), + authority_kind, + creator, + stored_bump: value.bump, + payload_json, + }); +} + +/// Historical Reservation List account generation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ReservationListAccountKind { + /// Legacy layout with `u8` spot counters. + V1, + /// Later historical layout with `u64` spot counters and cached totals. + V2, +} + +/// One bounded historical reservation entry. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReservationEntrySnapshot { + /// Reserved address. + pub address: String, + /// Spots still available for the address. + pub spots_remaining: u64, + /// Total spots assigned to the address. + pub total_spots: u64, +} + +/// Canonical decode-only projection of a historical Reservation List account. +#[derive(Clone, Debug, PartialEq)] +pub struct ReservationListAccountSnapshot { + /// Canonical account address. + pub account: String, + /// Canonical Reservation List PDA bump. + pub bump: u8, + /// Historical account generation. + pub kind: ReservationListAccountKind, + /// Master Edition stored by the account. + pub master_edition: String, + /// Resource key used by the PDA derivation. + pub resource: String, + /// Master Edition supply captured when reservations were created. + pub supply_snapshot: std::option::Option, + /// Bounded reservation entries. + pub reservations: std::vec::Vec, + /// Total reservation spots. V1 derives this value from entries. + pub total_reservation_spots: u64, + /// Current reservation spots. V1 derives this value from entries. + pub current_reservation_spots: u64, + /// Complete bounded JSON projection. + pub payload_json: serde_json::Value, +} + +fn read_reservation_u64(data: &[u8], offset: &mut usize) -> std::option::Option { + let end = match offset.checked_add(8) { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::option::Option::None, + }; + let bytes = match data.get(*offset..end) { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::option::Option::None, + }; + let array = match <[u8; 8]>::try_from(bytes) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + *offset = end; + return std::option::Option::Some(u64::from_le_bytes(array)); +} + +fn read_reservation_pubkey( + data: &[u8], + offset: &mut usize, +) -> std::option::Option { + let end = match offset.checked_add(32) { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::option::Option::None, + }; + let bytes = match data.get(*offset..end) { + std::option::Option::Some(value) => value, + std::option::Option::None => return std::option::Option::None, + }; + let array = match <[u8; 32]>::try_from(bytes) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return std::option::Option::None, + }; + *offset = end; + return std::option::Option::Some(solana_pubkey::Pubkey::new_from_array(array)); +} + +/// Decodes a historical Reservation List V1 or V2 account as decode-only state. +pub fn decode_reservation_list_account( + account: &str, + owner: &str, + resource: &str, + data: &[u8], +) -> std::result::Result { + const MAX_RESERVATIONS: usize = 200; + const MAX_V1_BYTES: usize = 6_949; + const MAX_V2_BYTES: usize = 9_749; + let account_key = match ::from_str(account) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let owner_key = match ::from_str(owner) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + let resource_key = match ::from_str(resource) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidAccountKey); + }, + }; + let program_key = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidOwner); + }, + }; + if owner_key != program_key { + return std::result::Result::Err(MetadataAccountDecodeError::ForeignOwner); + } + let key = match data.first() { + std::option::Option::Some(value) => *value, + std::option::Option::None => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidDataLength); + }, + }; + let (kind, maximum, entry_bytes) = match key { + 3 => (ReservationListAccountKind::V1, MAX_V1_BYTES, 34usize), + 5 => (ReservationListAccountKind::V2, MAX_V2_BYTES, 48usize), + _ => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidReservationListKey); + }, + }; + if data.len() > maximum { + return std::result::Result::Err(MetadataAccountDecodeError::ReservationListBoundsExceeded); + } + let mut offset = 1usize; + let master_edition = match read_reservation_pubkey(data, &mut offset) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let option_tag = match data.get(offset) { + std::option::Option::Some(value) => *value, + std::option::Option::None => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + offset += 1; + let supply_snapshot = match option_tag { + 0 => std::option::Option::None, + 1 => match read_reservation_u64(data, &mut offset) { + std::option::Option::Some(value) => std::option::Option::Some(value), + std::option::Option::None => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }, + _ => return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout), + }; + let count_end = match offset.checked_add(4) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let count_bytes = match data.get(offset..count_end) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let count_array = match <[u8; 4]>::try_from(count_bytes) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let count = u32::from_le_bytes(count_array) as usize; + offset = count_end; + if count > MAX_RESERVATIONS { + return std::result::Result::Err(MetadataAccountDecodeError::ReservationListBoundsExceeded); + } + let required_entries = match count.checked_mul(entry_bytes) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err( + MetadataAccountDecodeError::ReservationListBoundsExceeded, + ); + }, + }; + if data.len().saturating_sub(offset) < required_entries { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + } + let mut reservations = std::vec::Vec::with_capacity(count); + for _ in 0..count { + let address = match read_reservation_pubkey(data, &mut offset) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + }, + }; + let (spots_remaining, total_spots) = match kind { + ReservationListAccountKind::V1 => { + let remaining = match data.get(offset) { + std::option::Option::Some(value) => *value as u64, + std::option::Option::None => { + return std::result::Result::Err( + MetadataAccountDecodeError::InvalidMetadataLayout, + ); + }, + }; + let total = match data.get(offset + 1) { + std::option::Option::Some(value) => *value as u64, + std::option::Option::None => { + return std::result::Result::Err( + MetadataAccountDecodeError::InvalidMetadataLayout, + ); + }, + }; + offset += 2; + (remaining, total) + }, + ReservationListAccountKind::V2 => { + let remaining = match read_reservation_u64(data, &mut offset) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err( + MetadataAccountDecodeError::InvalidMetadataLayout, + ); + }, + }; + let total = match read_reservation_u64(data, &mut offset) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err( + MetadataAccountDecodeError::InvalidMetadataLayout, + ); + }, + }; + (remaining, total) + }, + }; + reservations.push(ReservationEntrySnapshot { + address: address.to_string(), + spots_remaining, + total_spots, + }); + } + let derived_total = reservations + .iter() + .fold(0u64, |sum, entry| return sum.saturating_add(entry.total_spots)); + let (total_reservation_spots, current_reservation_spots) = match kind { + ReservationListAccountKind::V1 => (derived_total, derived_total), + ReservationListAccountKind::V2 => { + let total = match read_reservation_u64(data, &mut offset) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err( + MetadataAccountDecodeError::InvalidMetadataLayout, + ); + }, + }; + let current = match read_reservation_u64(data, &mut offset) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err( + MetadataAccountDecodeError::InvalidMetadataLayout, + ); + }, + }; + (total, current) + }, + }; + if data + .get(offset..) + .is_some_and(|suffix| return suffix.iter().any(|byte| return *byte != 0)) + { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidMetadataLayout); + } + let (expected_pda, bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program_key.as_ref(), + master_edition.as_ref(), + b"reservation", + resource_key.as_ref(), + ], + &program_key, + ); + if account_key != expected_pda { + return std::result::Result::Err(MetadataAccountDecodeError::InvalidReservationListPda); + } + let payload_json = serde_json::json!({ + "kind": match kind { ReservationListAccountKind::V1 => "reservation_list_v1", ReservationListAccountKind::V2 => "reservation_list_v2" }, + "master_edition": master_edition.to_string(), + "resource": resource_key.to_string(), + "supply_snapshot": supply_snapshot, + "reservations": reservations.iter().map(|entry| return serde_json::json!({"address": entry.address.clone(), "spots_remaining": entry.spots_remaining, "total_spots": entry.total_spots})).collect::>(), + "total_reservation_spots": total_reservation_spots, + "current_reservation_spots": current_reservation_spots, + }); + return std::result::Result::Ok(ReservationListAccountSnapshot { + account: account_key.to_string(), + bump, + kind, + master_edition: master_edition.to_string(), + resource: resource_key.to_string(), + supply_snapshot, + reservations, + total_reservation_spots, + current_reservation_spots, + payload_json, + }); +} + +#[cfg(test)] +mod tests { + fn metadata() -> mpl_token_metadata::accounts::Metadata { + return mpl_token_metadata::accounts::Metadata { + key: mpl_token_metadata::types::Key::MetadataV1, + update_authority: mpl_token_metadata::ID, + mint: mpl_token_metadata::ID, + name: "Khadhroony".to_string(), + symbol: "KB".to_string(), + uri: "https://example.invalid/metadata.json".to_string(), + seller_fee_basis_points: 500, + creators: std::option::Option::None, + primary_sale_happened: false, + is_mutable: true, + edition_nonce: std::option::Option::None, + token_standard: std::option::Option::None, + collection: std::option::Option::None, + uses: std::option::Option::None, + collection_details: std::option::Option::None, + programmable_config: std::option::Option::None, + }; + } + + #[test] + fn metadata_account_decodes_with_exact_owner_and_pda() { + let metadata = metadata(); + let (account, expected_bump) = + mpl_token_metadata::accounts::Metadata::find_pda(&metadata.mint); + let data = match borsh_0_10::to_vec(&metadata) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("metadata serialization failed: {error}"), + }; + let snapshot = + match crate::decoder::metadata::metaplex_token_metadata::decode_metadata_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("metadata decode failed: {error}"), + }; + assert_eq!(snapshot.bump, expected_bump); + assert_eq!(snapshot.mint, metadata.mint.to_string()); + assert_eq!(snapshot.name, "Khadhroony"); + assert_eq!(snapshot.symbol, "KB"); + assert_eq!(snapshot.seller_fee_basis_points, 500); + assert!(snapshot.is_mutable); + assert!(!snapshot.primary_sale_happened); + } + + #[test] + fn metadata_account_rejects_foreign_owner_wrong_pda_and_bounds() { + let metadata = metadata(); + let (account, _) = mpl_token_metadata::accounts::Metadata::find_pda(&metadata.mint); + let data = match borsh_0_10::to_vec(&metadata) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("metadata serialization failed: {error}"), + }; + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_metadata_account( + &account.to_string(), + &solana_pubkey::Pubkey::new_unique().to_string(), + &data + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::ForeignOwner), + ); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_metadata_account( + &solana_pubkey::Pubkey::new_unique().to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &data + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidMetadataPda), + ); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_metadata_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &[] + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidDataLength), + ); + } + + #[test] + fn edition_and_master_edition_accounts_decode_with_exact_pda() { + let mint = mpl_token_metadata::ID; + let (account, expected_bump) = mpl_token_metadata::accounts::MasterEdition::find_pda(&mint); + let master_v2 = mpl_token_metadata::accounts::MasterEdition { + key: mpl_token_metadata::types::Key::MasterEditionV2, + supply: 7, + max_supply: std::option::Option::Some(100), + }; + let master_v2_data = match borsh_0_10::to_vec(&master_v2) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("master edition serialization failed: {error}") + }, + }; + let master_v2_snapshot = + match crate::decoder::metadata::metaplex_token_metadata::decode_edition_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &master_v2_data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("master edition decode failed: {error}"), + }; + assert_eq!(master_v2_snapshot.bump, expected_bump); + assert_eq!( + master_v2_snapshot.kind, + crate::decoder::metadata::metaplex_token_metadata::EditionAccountKind::MasterEditionV2 + ); + assert_eq!(master_v2_snapshot.supply, std::option::Option::Some(7)); + assert_eq!(master_v2_snapshot.max_supply, std::option::Option::Some(100)); + let edition = mpl_token_metadata::accounts::Edition { + key: mpl_token_metadata::types::Key::EditionV1, + parent: account, + edition: u64::MAX, + }; + let edition_data = match borsh_0_10::to_vec(&edition) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("edition serialization failed: {error}"), + }; + let edition_snapshot = + match crate::decoder::metadata::metaplex_token_metadata::decode_edition_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &edition_data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("edition decode failed: {error}"), + }; + assert_eq!( + edition_snapshot.kind, + crate::decoder::metadata::metaplex_token_metadata::EditionAccountKind::EditionV1 + ); + assert_eq!(edition_snapshot.parent, std::option::Option::Some(account.to_string())); + assert_eq!(edition_snapshot.edition, std::option::Option::Some(u64::MAX)); + } + + #[test] + fn historical_master_edition_v1_and_invalid_boundaries_are_explicit() { + let mint = mpl_token_metadata::ID; + let (account, _) = mpl_token_metadata::accounts::MasterEdition::find_pda(&mint); + let historical = mpl_token_metadata::accounts::DeprecatedMasterEditionV1 { + key: mpl_token_metadata::types::Key::MasterEditionV1, + supply: 1, + max_supply: std::option::Option::None, + printing_mint: mpl_token_metadata::ID, + one_time_printing_authorization_mint: mpl_token_metadata::ID, + }; + let data = match borsh_0_10::to_vec(&historical) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("historical master edition serialization failed: {error}") + }, + }; + let snapshot = + match crate::decoder::metadata::metaplex_token_metadata::decode_edition_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("historical master edition decode failed: {error}") + }, + }; + assert_eq!( + snapshot.kind, + crate::decoder::metadata::metaplex_token_metadata::EditionAccountKind::MasterEditionV1 + ); + assert_eq!( + snapshot.printing_mint, + std::option::Option::Some(mpl_token_metadata::ID.to_string()) + ); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_edition_account( + &solana_pubkey::Pubkey::new_unique().to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &data + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidMetadataPda), + ); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_edition_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &[255] + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidMetadataKey), + ); + } + + #[test] + fn edition_marker_v1_decodes_group_bitmap_and_exact_pda() { + let mint = mpl_token_metadata::ID.to_string(); + let program = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("program parse failed: {error}"), + }; + let mint_key = match ::from_str(&mint) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("mint parse failed: {error}"), + }; + let edition = 249u64; + let group = edition / 248; + let group_seed = group.to_string(); + let (account, expected_bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + mint_key.as_ref(), + b"edition", + group_seed.as_bytes(), + ], + &program, + ); + let mut ledger = [0u8; 31]; + ledger[0] = 64; + let marker = mpl_token_metadata::accounts::EditionMarker { + key: mpl_token_metadata::types::Key::EditionMarker, + ledger, + }; + let data = match borsh_0_10::to_vec(&marker) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("edition marker serialization failed: {error}") + }, + }; + let snapshot = + match crate::decoder::metadata::metaplex_token_metadata::decode_edition_marker_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint, + edition, + &data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("edition marker decode failed: {error}"), + }; + assert_eq!(snapshot.kind, crate::decoder::metadata::metaplex_token_metadata::EditionMarkerAccountKind::EditionMarkerV1); + assert_eq!(snapshot.bump, expected_bump); + assert_eq!(snapshot.marker_group, std::option::Option::Some(1)); + assert_eq!(snapshot.byte_index, 0); + assert_eq!(snapshot.bit_mask, 64); + assert!(snapshot.edition_taken); + } + + #[test] + fn edition_marker_v2_decodes_extensible_bitmap_and_rejects_boundaries() { + let mint = mpl_token_metadata::ID.to_string(); + let program = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("program parse failed: {error}"), + }; + let mint_key = match ::from_str(&mint) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("mint parse failed: {error}"), + }; + let (account, expected_bump) = solana_pubkey::Pubkey::find_program_address( + &[b"metadata", program.as_ref(), mint_key.as_ref(), b"edition", b"marker"], + &program, + ); + let marker = mpl_token_metadata::accounts::EditionMarkerV2 { + key: mpl_token_metadata::types::Key::EditionMarkerV2, + ledger: vec![0, 64], + }; + let data = match borsh_0_10::to_vec(&marker) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("edition marker v2 serialization failed: {error}") + }, + }; + let snapshot = + match crate::decoder::metadata::metaplex_token_metadata::decode_edition_marker_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint, + 9, + &data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("edition marker v2 decode failed: {error}") + }, + }; + assert_eq!(snapshot.kind, crate::decoder::metadata::metaplex_token_metadata::EditionMarkerAccountKind::EditionMarkerV2); + assert_eq!(snapshot.bump, expected_bump); + assert_eq!(snapshot.marker_group, std::option::Option::None); + assert_eq!(snapshot.byte_index, 1); + assert_eq!(snapshot.bit_mask, 64); + assert!(snapshot.edition_taken); + let mut suffixed = data.clone(); + suffixed.push(0); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_edition_marker_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint, + 9, + &suffixed + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidDataLength), + ); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_edition_marker_account( + &solana_pubkey::Pubkey::new_unique().to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint, + 9, + &data + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidEditionMarkerPda), + ); + } + + #[test] + fn token_record_decodes_programmable_state_delegate_and_exact_pda() { + let mint = solana_pubkey::Pubkey::new_unique(); + let token = solana_pubkey::Pubkey::new_unique(); + let program = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("program parse failed: {error}"), + }; + let (account, bump) = solana_pubkey::Pubkey::find_program_address( + &[b"metadata", program.as_ref(), mint.as_ref(), b"token_record", token.as_ref()], + &program, + ); + let value = mpl_token_metadata::accounts::TokenRecord { + key: mpl_token_metadata::types::Key::TokenRecord, + bump, + state: mpl_token_metadata::types::TokenState::Locked, + rule_set_revision: std::option::Option::Some(u64::MAX), + delegate: std::option::Option::Some(mpl_token_metadata::ID), + delegate_role: std::option::Option::Some( + mpl_token_metadata::types::TokenDelegateRole::Transfer, + ), + locked_transfer: std::option::Option::Some(mpl_token_metadata::ID), + }; + let mut data = match borsh_0_10::to_vec(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("token record serialization failed: {error}"), + }; + data.resize(mpl_token_metadata::accounts::TokenRecord::LEN, 0); + let snapshot = + match crate::decoder::metadata::metaplex_token_metadata::decode_token_record_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &token.to_string(), + &data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("token record decode failed: {error}"), + }; + assert_eq!(snapshot.bump, bump); + assert_eq!(snapshot.stored_bump, bump); + assert_eq!(snapshot.state, "Locked"); + assert!(snapshot.locked); + assert_eq!(snapshot.rule_set_revision, std::option::Option::Some(u64::MAX)); + assert_eq!(snapshot.delegate_role, std::option::Option::Some("Transfer".to_string())); + assert_eq!( + snapshot.delegate, + std::option::Option::Some(mpl_token_metadata::ID.to_string()) + ); + assert_eq!( + snapshot.locked_transfer, + std::option::Option::Some(mpl_token_metadata::ID.to_string()) + ); + } + + #[test] + fn token_record_rejects_foreign_owner_wrong_pda_bump_key_and_length() { + let mint = solana_pubkey::Pubkey::new_unique(); + let token = solana_pubkey::Pubkey::new_unique(); + let program = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("program parse failed: {error}"), + }; + let (account, bump) = solana_pubkey::Pubkey::find_program_address( + &[b"metadata", program.as_ref(), mint.as_ref(), b"token_record", token.as_ref()], + &program, + ); + let make_data = |key, stored_bump| { + let value = mpl_token_metadata::accounts::TokenRecord { + key, + bump: stored_bump, + state: mpl_token_metadata::types::TokenState::Unlocked, + rule_set_revision: std::option::Option::None, + delegate: std::option::Option::None, + delegate_role: std::option::Option::None, + locked_transfer: std::option::Option::None, + }; + let mut bytes = match borsh_0_10::to_vec(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("token record serialization failed: {error}") + }, + }; + bytes.resize(mpl_token_metadata::accounts::TokenRecord::LEN, 0); + return bytes; + }; + let data = make_data(mpl_token_metadata::types::Key::TokenRecord, bump); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_token_record_account( + &account.to_string(), + &solana_pubkey::Pubkey::new_unique().to_string(), + &mint.to_string(), + &token.to_string(), + &data + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::ForeignOwner), + ); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_token_record_account( + &solana_pubkey::Pubkey::new_unique().to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &token.to_string(), + &data + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidTokenRecordPda), + ); + let wrong_bump = + make_data(mpl_token_metadata::types::Key::TokenRecord, bump.wrapping_add(1)); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_token_record_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &token.to_string(), + &wrong_bump + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidTokenRecordPda), + ); + let wrong_key = make_data(mpl_token_metadata::types::Key::MetadataV1, bump); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_token_record_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &token.to_string(), + &wrong_key + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidTokenRecordKey), + ); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_token_record_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &token.to_string(), + &data[..data.len() - 1] + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidDataLength), + ); + } + + #[test] + fn delegate_records_decode_exact_role_seeds_and_pdas() { + let program = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("program parse failed: {error}"), + }; + let mint = mpl_token_metadata::ID; + let authority = mpl_token_metadata::ID; + let delegate = mpl_token_metadata::ID; + let role = mpl_token_metadata::types::MetadataDelegateRole::ProgrammableConfigItem; + let role_seed = role.to_string(); + let (account, bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + program.as_ref(), + role_seed.as_bytes(), + program.as_ref(), + program.as_ref(), + ], + &program, + ); + let record = mpl_token_metadata::accounts::MetadataDelegateRecord { + key: mpl_token_metadata::types::Key::MetadataDelegate, + bump, + mint, + delegate, + update_authority: authority, + }; + let data = match borsh_0_10::to_vec(&record) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("metadata delegate serialization failed: {error}") + }, + }; + assert_eq!(data.len(), mpl_token_metadata::accounts::MetadataDelegateRecord::LEN); + let snapshot = match crate::decoder::metadata::metaplex_token_metadata::decode_metadata_delegate_record_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + role, + &data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("metadata delegate decode failed: {error}"), + }; + assert_eq!(snapshot.role, "prog_config_item_delegate"); + assert_eq!(snapshot.stored_bump, bump); + let holder_role = mpl_token_metadata::types::HolderDelegateRole::PrintDelegate; + let holder_seed = holder_role.to_string(); + let (holder_account, holder_bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + program.as_ref(), + holder_seed.as_bytes(), + program.as_ref(), + program.as_ref(), + ], + &program, + ); + let holder_record = mpl_token_metadata::accounts::HolderDelegateRecord { + key: mpl_token_metadata::types::Key::HolderDelegate, + bump: holder_bump, + mint, + delegate, + update_authority: authority, + }; + let holder_data = match borsh_0_10::to_vec(&holder_record) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("holder delegate serialization failed: {error}") + }, + }; + let holder_snapshot = match crate::decoder::metadata::metaplex_token_metadata::decode_holder_delegate_record_account( + &holder_account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &authority.to_string(), + holder_role, + &holder_data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("holder delegate decode failed: {error}"), + }; + assert_eq!(holder_snapshot.role, "print_delegate"); + assert_eq!(holder_snapshot.stored_bump, holder_bump); + } + + #[test] + fn authority_records_decode_exact_values_and_reject_invalid_boundaries() { + let program = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("program parse failed: {error}"), + }; + let mint = mpl_token_metadata::ID; + let authority = mpl_token_metadata::ID; + let (collection_account, collection_bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + program.as_ref(), + b"collection_authority", + program.as_ref(), + ], + &program, + ); + let collection = mpl_token_metadata::accounts::CollectionAuthorityRecord { + key: mpl_token_metadata::types::Key::CollectionAuthorityRecord, + bump: collection_bump, + update_authority: std::option::Option::Some(mpl_token_metadata::ID), + }; + let collection_data = match borsh_0_10::to_vec(&collection) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("collection authority serialization failed: {error}") + }, + }; + let collection_snapshot = match crate::decoder::metadata::metaplex_token_metadata::decode_collection_authority_record_account( + &collection_account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &authority.to_string(), + &collection_data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("collection authority decode failed: {error}") + }, + }; + assert_eq!( + collection_snapshot.update_authority, + std::option::Option::Some(mpl_token_metadata::ID.to_string()) + ); + let (use_account, use_bump) = solana_pubkey::Pubkey::find_program_address( + &[b"metadata", program.as_ref(), program.as_ref(), b"user", program.as_ref()], + &program, + ); + let use_record = mpl_token_metadata::accounts::UseAuthorityRecord { + key: mpl_token_metadata::types::Key::UseAuthorityRecord, + allowed_uses: u64::MAX, + bump: use_bump, + }; + let use_data = match borsh_0_10::to_vec(&use_record) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("use authority serialization failed: {error}") + }, + }; + let use_snapshot = match crate::decoder::metadata::metaplex_token_metadata::decode_use_authority_record_account( + &use_account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &authority.to_string(), + &use_data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("use authority decode failed: {error}"), + }; + assert_eq!(use_snapshot.allowed_uses, std::option::Option::Some(u64::MAX)); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_use_authority_record_account( + &solana_pubkey::Pubkey::new_unique().to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &authority.to_string(), + &use_data + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidAuthorityRecordPda) + ); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_use_authority_record_account( + &use_account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &mint.to_string(), + &authority.to_string(), + &use_data[..9] + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidDataLength) + ); + } + + #[test] + fn token_owned_escrow_decodes_token_owner_and_creator_authorities() { + let program = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("program parse failed: {error}"), + }; + let base_token = mpl_token_metadata::ID; + let base_key = program; + let (owner_account, owner_bump) = solana_pubkey::Pubkey::find_program_address( + &[b"metadata", program.as_ref(), base_key.as_ref(), &[0], b"escrow"], + &program, + ); + let owner_value = mpl_token_metadata::accounts::TokenOwnedEscrow { + key: mpl_token_metadata::types::Key::TokenOwnedEscrow, + base_token, + authority: mpl_token_metadata::types::EscrowAuthority::TokenOwner, + bump: owner_bump, + }; + let owner_data = match borsh_0_10::to_vec(&owner_value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("escrow serialization failed: {error}"), + }; + assert_eq!(owner_data.len(), 35); + let owner_snapshot = match crate::decoder::metadata::metaplex_token_metadata::decode_token_owned_escrow_account( + &owner_account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &owner_data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("escrow decode failed: {error}"), + }; + assert_eq!(owner_snapshot.authority_kind, crate::decoder::metadata::metaplex_token_metadata::TokenOwnedEscrowAuthorityKind::TokenOwner); + assert_eq!(owner_snapshot.creator, std::option::Option::None); + let creator = mpl_token_metadata::ID; + let (creator_account, creator_bump) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + base_key.as_ref(), + &[1], + program.as_ref(), + b"escrow", + ], + &program, + ); + let creator_value = mpl_token_metadata::accounts::TokenOwnedEscrow { + key: mpl_token_metadata::types::Key::TokenOwnedEscrow, + base_token, + authority: mpl_token_metadata::types::EscrowAuthority::Creator(creator), + bump: creator_bump, + }; + let creator_data = match borsh_0_10::to_vec(&creator_value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("creator escrow serialization failed: {error}") + }, + }; + assert_eq!(creator_data.len(), 67); + let creator_snapshot = match crate::decoder::metadata::metaplex_token_metadata::decode_token_owned_escrow_account( + &creator_account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &creator_data, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("creator escrow decode failed: {error}"), + }; + assert_eq!(creator_snapshot.authority_kind, crate::decoder::metadata::metaplex_token_metadata::TokenOwnedEscrowAuthorityKind::Creator); + assert_eq!( + creator_snapshot.creator, + std::option::Option::Some(mpl_token_metadata::ID.to_string()) + ); + } + + #[test] + fn token_owned_escrow_rejects_wrong_pda_bump_key_and_length() { + let program = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("program parse failed: {error}"), + }; + let (account, bump) = solana_pubkey::Pubkey::find_program_address( + &[b"metadata", program.as_ref(), program.as_ref(), &[0], b"escrow"], + &program, + ); + let make = |key, stored_bump| { + let value = mpl_token_metadata::accounts::TokenOwnedEscrow { + key, + base_token: mpl_token_metadata::ID, + authority: mpl_token_metadata::types::EscrowAuthority::TokenOwner, + bump: stored_bump, + }; + return match borsh_0_10::to_vec(&value) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("escrow serialization failed: {error}"), + }; + }; + let data = make(mpl_token_metadata::types::Key::TokenOwnedEscrow, bump); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_token_owned_escrow_account( + &solana_pubkey::Pubkey::new_unique().to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &data + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidTokenOwnedEscrowPda) + ); + let wrong_bump = + make(mpl_token_metadata::types::Key::TokenOwnedEscrow, bump.wrapping_add(1)); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_token_owned_escrow_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &wrong_bump + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidTokenOwnedEscrowPda) + ); + let wrong_key = make(mpl_token_metadata::types::Key::MetadataV1, bump); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_token_owned_escrow_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &wrong_key + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidTokenOwnedEscrowKey) + ); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_token_owned_escrow_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &data[..34] + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidDataLength) + ); + } + + #[test] + fn historical_reservation_lists_decode_v1_and_v2_exactly() { + let program = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("program parse failed: {error}"), + }; + let master = solana_pubkey::Pubkey::new_unique(); + let resource = solana_pubkey::Pubkey::new_unique(); + let reserved = solana_pubkey::Pubkey::new_unique(); + let (account, _) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + master.as_ref(), + b"reservation", + resource.as_ref(), + ], + &program, + ); + let mut v1 = vec![3u8]; + v1.extend_from_slice(master.as_ref()); + v1.push(1); + v1.extend_from_slice(&9u64.to_le_bytes()); + v1.extend_from_slice(&1u32.to_le_bytes()); + v1.extend_from_slice(reserved.as_ref()); + v1.push(2); + v1.push(3); + let snapshot = + match crate::decoder::metadata::metaplex_token_metadata::decode_reservation_list_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &resource.to_string(), + &v1, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("reservation v1 decode failed: {error}"), + }; + assert_eq!( + snapshot.kind, + crate::decoder::metadata::metaplex_token_metadata::ReservationListAccountKind::V1 + ); + assert_eq!(snapshot.supply_snapshot, std::option::Option::Some(9)); + assert_eq!(snapshot.total_reservation_spots, 3); + let mut v2 = vec![5u8]; + v2.extend_from_slice(master.as_ref()); + v2.push(0); + v2.extend_from_slice(&1u32.to_le_bytes()); + v2.extend_from_slice(reserved.as_ref()); + v2.extend_from_slice(&u64::MAX.to_le_bytes()); + v2.extend_from_slice(&7u64.to_le_bytes()); + v2.extend_from_slice(&11u64.to_le_bytes()); + v2.extend_from_slice(&5u64.to_le_bytes()); + let snapshot = + match crate::decoder::metadata::metaplex_token_metadata::decode_reservation_list_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &resource.to_string(), + &v2, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("reservation v2 decode failed: {error}"), + }; + assert_eq!( + snapshot.kind, + crate::decoder::metadata::metaplex_token_metadata::ReservationListAccountKind::V2 + ); + assert_eq!(snapshot.reservations[0].spots_remaining, u64::MAX); + assert_eq!(snapshot.total_reservation_spots, 11); + assert_eq!(snapshot.current_reservation_spots, 5); + } + + #[test] + fn published_account_key_inventory_is_exact_and_exhaustive() { + let variants = [ + mpl_token_metadata::types::Key::Uninitialized, + mpl_token_metadata::types::Key::EditionV1, + mpl_token_metadata::types::Key::MasterEditionV1, + mpl_token_metadata::types::Key::ReservationListV1, + mpl_token_metadata::types::Key::MetadataV1, + mpl_token_metadata::types::Key::ReservationListV2, + mpl_token_metadata::types::Key::MasterEditionV2, + mpl_token_metadata::types::Key::EditionMarker, + mpl_token_metadata::types::Key::UseAuthorityRecord, + mpl_token_metadata::types::Key::CollectionAuthorityRecord, + mpl_token_metadata::types::Key::TokenOwnedEscrow, + mpl_token_metadata::types::Key::TokenRecord, + mpl_token_metadata::types::Key::MetadataDelegate, + mpl_token_metadata::types::Key::EditionMarkerV2, + mpl_token_metadata::types::Key::HolderDelegate, + ]; + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::METADATA_ACCOUNT_LAYOUT_AUDIT.len(), + variants.len() + ); + for (index, variant) in variants.iter().enumerate() { + let bytes = match borsh_0_10::to_vec(variant) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("key serialization failed: {error}"), + }; + assert_eq!(bytes, vec![index as u8]); + let entry = + crate::decoder::metadata::metaplex_token_metadata::METADATA_ACCOUNT_LAYOUT_AUDIT + [index]; + assert_eq!(entry.discriminant, index as u8); + assert!(!entry.key_name.is_empty()); + assert!(!entry.decoder.is_empty()); + assert!(!entry.lifecycle.is_empty()); + } + } + + #[test] + fn account_audit_matrix_covers_every_key_without_duplicate_discriminants() { + let mut discriminants = std::collections::BTreeSet::new(); + let mut names = std::collections::BTreeSet::new(); + for entry in + crate::decoder::metadata::metaplex_token_metadata::METADATA_ACCOUNT_LAYOUT_AUDIT + { + assert!(discriminants.insert(entry.discriminant)); + assert!(names.insert(entry.key_name)); + if entry.discriminant == 0 { + assert_eq!(entry.decoder, "reject_uninitialized"); + } else { + assert_ne!(entry.decoder, "reject_uninitialized"); + } + } + let matrix: serde_json::Value = match serde_json::from_str(include_str!( + "../../../../../docs/METAPLEX_TOKEN_METADATA_MATRIX.json" + )) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("matrix parse failed: {error}"), + }; + let inventory = + match matrix.get("account_key_inventory").and_then(serde_json::Value::as_array) { + std::option::Option::Some(value) => value, + std::option::Option::None => panic!("account_key_inventory missing"), + }; + assert_eq!( + inventory.len(), + crate::decoder::metadata::metaplex_token_metadata::METADATA_ACCOUNT_LAYOUT_AUDIT.len() + ); + for (index, item) in inventory.iter().enumerate() { + assert_eq!( + item.get("discriminant").and_then(serde_json::Value::as_u64), + std::option::Option::Some(index as u64) + ); + assert_eq!( + item.get("key").and_then(serde_json::Value::as_str), + std::option::Option::Some(crate::decoder::metadata::metaplex_token_metadata::METADATA_ACCOUNT_LAYOUT_AUDIT[index].key_name) + ); + assert_eq!( + item.get("decoder").and_then(serde_json::Value::as_str), + std::option::Option::Some(crate::decoder::metadata::metaplex_token_metadata::METADATA_ACCOUNT_LAYOUT_AUDIT[index].decoder) + ); + } + } + + #[test] + fn historical_reservation_lists_reject_wrong_pda_key_bounds_and_suffix() { + let program = match ::from_str( + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => panic!("program parse failed: {error}"), + }; + let master = solana_pubkey::Pubkey::new_unique(); + let resource = solana_pubkey::Pubkey::new_unique(); + let (account, _) = solana_pubkey::Pubkey::find_program_address( + &[ + b"metadata", + program.as_ref(), + master.as_ref(), + b"reservation", + resource.as_ref(), + ], + &program, + ); + let mut data = vec![3u8]; + data.extend_from_slice(master.as_ref()); + data.push(0); + data.extend_from_slice(&0u32.to_le_bytes()); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_reservation_list_account( + &solana_pubkey::Pubkey::new_unique().to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &resource.to_string(), + &data + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidReservationListPda) + ); + let mut wrong_key = data.clone(); + wrong_key[0] = 4; + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_reservation_list_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &resource.to_string(), + &wrong_key + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidReservationListKey) + ); + let mut too_many = data.clone(); + too_many[34..38].copy_from_slice(&201u32.to_le_bytes()); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_reservation_list_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &resource.to_string(), + &too_many + ), + std::result::Result::Err( + crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::ReservationListBoundsExceeded + ) + ); + let mut suffix = data.clone(); + suffix.push(1); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::decode_reservation_list_account( + &account.to_string(), + kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + &resource.to_string(), + &suffix + ), + std::result::Result::Err(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountDecodeError::InvalidMetadataLayout) + ); + } +} diff --git a/kb-lib/src/decoder/metadata/metaplex_token_metadata/canonical.rs b/kb-lib/src/decoder/metadata/metaplex_token_metadata/canonical.rs new file mode 100644 index 0000000..702ec94 --- /dev/null +++ b/kb-lib/src/decoder/metadata/metaplex_token_metadata/canonical.rs @@ -0,0 +1,425 @@ +// file: kb-lib/src/decoder/metadata/metaplex_token_metadata/canonical.rs +// version: 2 + +//! Canonical Metaplex account identities, provenance and materialization policy. + +/// Lifecycle class of one decoded Metaplex account layout. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CanonicalAccountLifecycle { + /// Current account layout used by supported runtime flows. + Active, + /// Published account layout whose runtime surface is still experimental. + ActiveExperimental, + /// Historical account layout retained for ledger replay and backfill. + Historical, +} + +/// Materialization policy attached to one canonical account snapshot. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CanonicalMaterializationPolicy { + /// Materialize the latest committed state of an active account. + ActiveState, + /// Materialize historical state with explicit lifecycle provenance. + HistoricalState, +} + +/// Execution policy attached to one canonical account layout. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CanonicalExecutionPolicy { + /// The layout belongs to current operations that may later be executable. + CurrentCandidate, + /// The layout belongs to an experimental operation requiring explicit activation. + ExperimentalCandidate, + /// Historical state must never be exposed by an executor. + NeverHistorical, +} + +/// Origin of one canonical account observation. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CanonicalObservationSource { + /// Account observed from a live subscription or direct live read. + Live, + /// Account reconstructed while replaying already stored transactions. + Replay, + /// Account discovered while backfilling historical ledger data. + Backfill, + /// Account read to repair a detected ingestion gap. + Repair, + /// Synthetic account used only by bounded validation tests. + Synthetic, +} + +/// Provenance retained for one canonical account observation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CanonicalAccountProvenance { + /// Observation source. + pub source: CanonicalObservationSource, + /// Slot associated with the account state when known. + pub slot: std::option::Option, + /// Provider write version when available. + pub write_version: std::option::Option, + /// Transaction signature responsible for the state when known. + pub signature: std::option::Option, + /// Instruction index associated with the mutation when known. + pub instruction_index: std::option::Option, + /// Inner-instruction index associated with the mutation when known. + pub inner_instruction_index: std::option::Option, + /// Whether the source state is committed according to the caller's boundary. + pub committed: bool, +} + +/// Stable identity and policy of one canonical Metaplex account snapshot. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CanonicalAccountIdentity { + /// Stable identity formed from program id and canonical account address. + pub stable_id: String, + /// Canonical Metaplex Token Metadata program id. + pub program_id: String, + /// Canonical account address. + pub account: String, + /// Published layout name. + pub layout: String, + /// Exact published `Key` discriminant. + pub discriminant: u8, + /// Internal canonical-model version. + pub canonical_version: u16, + /// Published lifecycle class. + pub lifecycle: CanonicalAccountLifecycle, + /// Materialization policy, including historical backfill state. + pub materialization: CanonicalMaterializationPolicy, + /// Execution policy separated from materialization. + pub execution: CanonicalExecutionPolicy, +} + +/// Typed state carried by one canonical Metaplex account snapshot. +#[derive(Clone, Debug, PartialEq)] +pub enum CanonicalMetadataAccountState { + /// Metadata account state. + Metadata(crate::decoder::metadata::metaplex_token_metadata::MetadataAccountSnapshot), + /// Master Edition or printed Edition state. + Edition(crate::decoder::metadata::metaplex_token_metadata::EditionAccountSnapshot), + /// Edition Marker state. + EditionMarker(crate::decoder::metadata::metaplex_token_metadata::EditionMarkerAccountSnapshot), + /// Programmable Token Record state. + TokenRecord(crate::decoder::metadata::metaplex_token_metadata::TokenRecordAccountSnapshot), + /// Metadata or holder delegate state. + DelegateRecord( + crate::decoder::metadata::metaplex_token_metadata::DelegateRecordAccountSnapshot, + ), + /// Collection or use authority state. + AuthorityRecord( + crate::decoder::metadata::metaplex_token_metadata::AuthorityRecordAccountSnapshot, + ), + /// Token-owned escrow state. + TokenOwnedEscrow( + crate::decoder::metadata::metaplex_token_metadata::TokenOwnedEscrowAccountSnapshot, + ), + /// Historical Reservation List state. + ReservationList( + crate::decoder::metadata::metaplex_token_metadata::ReservationListAccountSnapshot, + ), +} + +/// Canonical envelope used by future materializers and stateful replay. +#[derive(Clone, Debug, PartialEq)] +pub struct CanonicalMetadataAccountSnapshot { + /// Stable identity and lifecycle policy. + pub identity: CanonicalAccountIdentity, + /// Observation provenance. + pub provenance: CanonicalAccountProvenance, + /// Typed bounded state. + pub state: CanonicalMetadataAccountState, +} + +fn canonical_identity( + account: &str, + layout: &str, + discriminant: u8, + lifecycle: CanonicalAccountLifecycle, +) -> CanonicalAccountIdentity { + let program_id = kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID.to_string(); + let materialization = match lifecycle { + CanonicalAccountLifecycle::Historical => CanonicalMaterializationPolicy::HistoricalState, + CanonicalAccountLifecycle::Active | CanonicalAccountLifecycle::ActiveExperimental => { + CanonicalMaterializationPolicy::ActiveState + }, + }; + let execution = match lifecycle { + CanonicalAccountLifecycle::Historical => CanonicalExecutionPolicy::NeverHistorical, + CanonicalAccountLifecycle::Active => CanonicalExecutionPolicy::CurrentCandidate, + CanonicalAccountLifecycle::ActiveExperimental => { + CanonicalExecutionPolicy::ExperimentalCandidate + }, + }; + return CanonicalAccountIdentity { + stable_id: format!("{program_id}:{account}"), + program_id, + account: account.to_string(), + layout: layout.to_string(), + discriminant, + canonical_version: 1, + lifecycle, + materialization, + execution, + }; +} + +impl CanonicalMetadataAccountSnapshot { + /// Wraps one Metadata account snapshot with stable identity and provenance. + pub fn from_metadata( + snapshot: crate::decoder::metadata::metaplex_token_metadata::MetadataAccountSnapshot, + provenance: CanonicalAccountProvenance, + ) -> Self { + let identity = canonical_identity( + &snapshot.account, + "MetadataV1", + mpl_token_metadata::types::Key::MetadataV1 as u8, + CanonicalAccountLifecycle::Active, + ); + return Self { + identity, + provenance, + state: CanonicalMetadataAccountState::Metadata(snapshot), + }; + } + + /// Wraps one Edition or Master Edition snapshot with exact lifecycle policy. + pub fn from_edition( + snapshot: crate::decoder::metadata::metaplex_token_metadata::EditionAccountSnapshot, + provenance: CanonicalAccountProvenance, + ) -> Self { + let (layout, discriminant, lifecycle) = match snapshot.kind { + crate::decoder::metadata::metaplex_token_metadata::EditionAccountKind::MasterEditionV1 => ( + "MasterEditionV1", + mpl_token_metadata::types::Key::MasterEditionV1 as u8, + CanonicalAccountLifecycle::Historical, + ), + crate::decoder::metadata::metaplex_token_metadata::EditionAccountKind::MasterEditionV2 => ( + "MasterEditionV2", + mpl_token_metadata::types::Key::MasterEditionV2 as u8, + CanonicalAccountLifecycle::Active, + ), + crate::decoder::metadata::metaplex_token_metadata::EditionAccountKind::EditionV1 => ( + "EditionV1", + mpl_token_metadata::types::Key::EditionV1 as u8, + CanonicalAccountLifecycle::Active, + ), + }; + let identity = canonical_identity(&snapshot.account, layout, discriminant, lifecycle); + return Self { + identity, + provenance, + state: CanonicalMetadataAccountState::Edition(snapshot), + }; + } + + /// Wraps one Edition Marker snapshot with exact lifecycle policy. + pub fn from_edition_marker( + snapshot: crate::decoder::metadata::metaplex_token_metadata::EditionMarkerAccountSnapshot, + provenance: CanonicalAccountProvenance, + ) -> Self { + let (layout, discriminant, lifecycle) = match snapshot.kind { + crate::decoder::metadata::metaplex_token_metadata::EditionMarkerAccountKind::EditionMarkerV1 => ( + "EditionMarker", + mpl_token_metadata::types::Key::EditionMarker as u8, + CanonicalAccountLifecycle::Active, + ), + crate::decoder::metadata::metaplex_token_metadata::EditionMarkerAccountKind::EditionMarkerV2 => ( + "EditionMarkerV2", + mpl_token_metadata::types::Key::EditionMarkerV2 as u8, + CanonicalAccountLifecycle::ActiveExperimental, + ), + }; + let identity = canonical_identity(&snapshot.account, layout, discriminant, lifecycle); + return Self { + identity, + provenance, + state: CanonicalMetadataAccountState::EditionMarker(snapshot), + }; + } + + /// Wraps one Token Record snapshot. + pub fn from_token_record( + snapshot: crate::decoder::metadata::metaplex_token_metadata::TokenRecordAccountSnapshot, + provenance: CanonicalAccountProvenance, + ) -> Self { + let identity = canonical_identity( + &snapshot.account, + "TokenRecord", + mpl_token_metadata::types::Key::TokenRecord as u8, + CanonicalAccountLifecycle::Active, + ); + return Self { + identity, + provenance, + state: CanonicalMetadataAccountState::TokenRecord(snapshot), + }; + } + + /// Wraps one Metadata or Holder Delegate snapshot. + pub fn from_delegate_record( + snapshot: crate::decoder::metadata::metaplex_token_metadata::DelegateRecordAccountSnapshot, + provenance: CanonicalAccountProvenance, + ) -> Self { + let (layout, discriminant) = if snapshot.kind == "MetadataDelegateRecord" { + ("MetadataDelegate", mpl_token_metadata::types::Key::MetadataDelegate as u8) + } else { + ("HolderDelegate", mpl_token_metadata::types::Key::HolderDelegate as u8) + }; + let identity = canonical_identity( + &snapshot.account, + layout, + discriminant, + CanonicalAccountLifecycle::Active, + ); + return Self { + identity, + provenance, + state: CanonicalMetadataAccountState::DelegateRecord(snapshot), + }; + } + + /// Wraps one Collection or Use Authority snapshot. + pub fn from_authority_record( + snapshot: crate::decoder::metadata::metaplex_token_metadata::AuthorityRecordAccountSnapshot, + provenance: CanonicalAccountProvenance, + ) -> Self { + let (layout, discriminant) = if snapshot.kind == "CollectionAuthorityRecord" { + ( + "CollectionAuthorityRecord", + mpl_token_metadata::types::Key::CollectionAuthorityRecord as u8, + ) + } else { + ("UseAuthorityRecord", mpl_token_metadata::types::Key::UseAuthorityRecord as u8) + }; + let identity = canonical_identity( + &snapshot.account, + layout, + discriminant, + CanonicalAccountLifecycle::Active, + ); + return Self { + identity, + provenance, + state: CanonicalMetadataAccountState::AuthorityRecord(snapshot), + }; + } + + /// Wraps one Token Owned Escrow snapshot. + pub fn from_token_owned_escrow( + snapshot: crate::decoder::metadata::metaplex_token_metadata::TokenOwnedEscrowAccountSnapshot, + provenance: CanonicalAccountProvenance, + ) -> Self { + let identity = canonical_identity( + &snapshot.account, + "TokenOwnedEscrow", + mpl_token_metadata::types::Key::TokenOwnedEscrow as u8, + CanonicalAccountLifecycle::Active, + ); + return Self { + identity, + provenance, + state: CanonicalMetadataAccountState::TokenOwnedEscrow(snapshot), + }; + } + + /// Wraps one historical Reservation List snapshot as materializable historical state. + pub fn from_reservation_list( + snapshot: crate::decoder::metadata::metaplex_token_metadata::ReservationListAccountSnapshot, + provenance: CanonicalAccountProvenance, + ) -> Self { + let (layout, discriminant) = match snapshot.kind { + crate::decoder::metadata::metaplex_token_metadata::ReservationListAccountKind::V1 => { + ("ReservationListV1", mpl_token_metadata::types::Key::ReservationListV1 as u8) + }, + crate::decoder::metadata::metaplex_token_metadata::ReservationListAccountKind::V2 => { + ("ReservationListV2", mpl_token_metadata::types::Key::ReservationListV2 as u8) + }, + }; + let identity = canonical_identity( + &snapshot.account, + layout, + discriminant, + CanonicalAccountLifecycle::Historical, + ); + return Self { + identity, + provenance, + state: CanonicalMetadataAccountState::ReservationList(snapshot), + }; + } +} + +#[cfg(test)] +mod tests { + #[test] + fn historical_state_is_materializable_but_never_executable() { + let snapshot = crate::decoder::metadata::metaplex_token_metadata::ReservationListAccountSnapshot { + account: "reservation-account".to_string(), + bump: 1, + kind: crate::decoder::metadata::metaplex_token_metadata::ReservationListAccountKind::V1, + master_edition: "master-edition".to_string(), + resource: "resource".to_string(), + supply_snapshot: std::option::Option::Some(7), + reservations: std::vec::Vec::new(), + total_reservation_spots: 0, + current_reservation_spots: 0, + payload_json: serde_json::json!({}), + }; + let provenance = super::CanonicalAccountProvenance { + source: super::CanonicalObservationSource::Backfill, + slot: std::option::Option::Some(42), + write_version: std::option::Option::None, + signature: std::option::Option::Some("historical-signature".to_string()), + instruction_index: std::option::Option::Some(3), + inner_instruction_index: std::option::Option::None, + committed: true, + }; + let canonical = + super::CanonicalMetadataAccountSnapshot::from_reservation_list(snapshot, provenance); + assert_eq!(canonical.identity.lifecycle, super::CanonicalAccountLifecycle::Historical); + assert_eq!( + canonical.identity.materialization, + super::CanonicalMaterializationPolicy::HistoricalState + ); + assert_eq!(canonical.identity.execution, super::CanonicalExecutionPolicy::NeverHistorical); + assert_eq!(canonical.provenance.source, super::CanonicalObservationSource::Backfill); + assert!(canonical.identity.stable_id.ends_with(":reservation-account")); + } + + #[test] + fn active_and_experimental_layouts_keep_distinct_policies() { + let metadata = crate::decoder::metadata::metaplex_token_metadata::MetadataAccountSnapshot { + account: "metadata-account".to_string(), + bump: 1, + mint: "mint".to_string(), + update_authority: "authority".to_string(), + name: "name".to_string(), + symbol: "symbol".to_string(), + uri: "uri".to_string(), + seller_fee_basis_points: 0, + primary_sale_happened: false, + is_mutable: true, + payload_json: serde_json::json!({}), + }; + let provenance = super::CanonicalAccountProvenance { + source: super::CanonicalObservationSource::Live, + slot: std::option::Option::Some(1), + write_version: std::option::Option::Some(2), + signature: std::option::Option::None, + instruction_index: std::option::Option::None, + inner_instruction_index: std::option::Option::None, + committed: true, + }; + let canonical = + super::CanonicalMetadataAccountSnapshot::from_metadata(metadata, provenance); + assert_eq!(canonical.identity.lifecycle, super::CanonicalAccountLifecycle::Active); + assert_eq!( + canonical.identity.materialization, + super::CanonicalMaterializationPolicy::ActiveState + ); + assert_eq!(canonical.identity.execution, super::CanonicalExecutionPolicy::CurrentCandidate); + assert_eq!(canonical.identity.discriminant, 4); + assert_eq!(canonical.identity.canonical_version, 1); + } +} diff --git a/kb-lib/src/decoder/metadata/metaplex_token_metadata/constants.rs b/kb-lib/src/decoder/metadata/metaplex_token_metadata/constants.rs new file mode 100644 index 0000000..e169601 --- /dev/null +++ b/kb-lib/src/decoder/metadata/metaplex_token_metadata/constants.rs @@ -0,0 +1,257 @@ +// file: kb-lib/src/decoder/metadata/metaplex_token_metadata/constants.rs +// version: 29 + +//! Internal constants for Metaplex Token Metadata decoding. + +/// Stable decoder surface code. +pub(crate) const SURFACE_CODE: &str = "metadata_metaplex_token_metadata"; +/// Maximum retained instruction payload size. +pub(crate) const MAX_INSTRUCTION_BYTES: usize = 16_384; +/// Maximum encoded base64 instruction payload size. +pub(crate) const MAX_BASE64_BYTES: usize = 21_852; +/// Maximum number of instruction accounts accepted by this decoder. +pub(crate) const MAX_INSTRUCTION_ACCOUNTS: usize = 64; +/// Number of bytes retained in malformed-wire diagnostics. +pub(crate) const DIAGNOSTIC_PREFIX_BYTES: usize = 32; +/// Stable event payload contract version. +pub(crate) const EVENT_VERSION: u32 = 1; +/// Deprecated Create Master Edition discriminator. +pub(crate) const DEPRECATED_CREATE_MASTER_EDITION_DISCRIMINATOR: u8 = 2; +/// Deprecated Set Reservation List discriminator. +pub(crate) const DEPRECATED_SET_RESERVATION_LIST_DISCRIMINATOR: u8 = 5; +/// Deprecated Create Reservation List discriminator. +pub(crate) const DEPRECATED_CREATE_RESERVATION_LIST_DISCRIMINATOR: u8 = 6; +/// Deprecated Mint Printing Tokens Via Token discriminator. +pub(crate) const DEPRECATED_MINT_PRINTING_TOKENS_VIA_TOKEN_DISCRIMINATOR: u8 = 8; +/// Deprecated Mint Printing Tokens discriminator. +pub(crate) const DEPRECATED_MINT_PRINTING_TOKENS_DISCRIMINATOR: u8 = 9; +/// Historical Create Master Edition discriminator. +pub(crate) const CREATE_MASTER_EDITION_DISCRIMINATOR: u8 = 10; +/// Deprecated printing-token mint-new-edition discriminator. +pub(crate) const DEPRECATED_MINT_NEW_EDITION_VIA_PRINTING_TOKEN_DISCRIMINATOR: u8 = 3; +/// Historical Create Metadata Account discriminator. +pub(crate) const CREATE_METADATA_ACCOUNT_DISCRIMINATOR: u8 = 0; +/// Historical Update Metadata Account discriminator. +pub(crate) const UPDATE_METADATA_ACCOUNT_DISCRIMINATOR: u8 = 1; +/// Historical Create Metadata Account V2 discriminator. +pub(crate) const CREATE_METADATA_ACCOUNT_V2_DISCRIMINATOR: u8 = 16; +/// Create Metadata Account V3 discriminator. +pub(crate) const CREATE_METADATA_ACCOUNT_V3_DISCRIMINATOR: u8 = 33; +/// Update Metadata Account V2 discriminator. +pub(crate) const UPDATE_METADATA_ACCOUNT_V2_DISCRIMINATOR: u8 = 15; +/// Verify Collection discriminator. +pub(crate) const VERIFY_COLLECTION_DISCRIMINATOR: u8 = 18; +/// Unverify Collection discriminator. +pub(crate) const UNVERIFY_COLLECTION_DISCRIMINATOR: u8 = 22; +/// Set And Verify Collection discriminator. +pub(crate) const SET_AND_VERIFY_COLLECTION_DISCRIMINATOR: u8 = 25; +/// Approve Collection Authority discriminator. +pub(crate) const APPROVE_COLLECTION_AUTHORITY_DISCRIMINATOR: u8 = 23; +/// Revoke Collection Authority discriminator. +pub(crate) const REVOKE_COLLECTION_AUTHORITY_DISCRIMINATOR: u8 = 24; +/// Verify Sized Collection Item discriminator. +pub(crate) const VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR: u8 = 30; +/// Unverify Sized Collection Item discriminator. +pub(crate) const UNVERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR: u8 = 31; +/// Set And Verify Sized Collection Item discriminator. +pub(crate) const SET_AND_VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR: u8 = 32; +/// Sign Metadata discriminator. +pub(crate) const SIGN_METADATA_DISCRIMINATOR: u8 = 7; +/// Update Primary Sale Happened Via Token discriminator. +pub(crate) const UPDATE_PRIMARY_SALE_HAPPENED_VIA_TOKEN_DISCRIMINATOR: u8 = 4; +/// Utilize discriminator. +pub(crate) const UTILIZE_DISCRIMINATOR: u8 = 19; +/// Approve Use Authority discriminator. +pub(crate) const APPROVE_USE_AUTHORITY_DISCRIMINATOR: u8 = 20; +/// Revoke Use Authority discriminator. +pub(crate) const REVOKE_USE_AUTHORITY_DISCRIMINATOR: u8 = 21; +/// Puff Metadata discriminator. +pub(crate) const PUFF_METADATA_DISCRIMINATOR: u8 = 14; +/// Freeze Delegated Account discriminator. +pub(crate) const FREEZE_DELEGATED_ACCOUNT_DISCRIMINATOR: u8 = 26; +/// Thaw Delegated Account discriminator. +pub(crate) const THAW_DELEGATED_ACCOUNT_DISCRIMINATOR: u8 = 27; +/// Remove Creator Verification discriminator. +pub(crate) const REMOVE_CREATOR_VERIFICATION_DISCRIMINATOR: u8 = 28; +/// Set Token Standard discriminator. +pub(crate) const SET_TOKEN_STANDARD_DISCRIMINATOR: u8 = 35; +/// Mint New Edition From Master Edition Via Token discriminator. +pub(crate) const MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_TOKEN_DISCRIMINATOR: u8 = 11; +/// Mint New Edition From Master Edition Via Vault Proxy discriminator. +pub(crate) const MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_VAULT_PROXY_DISCRIMINATOR: u8 = 13; +/// Convert Master Edition V1 to V2 discriminator. +pub(crate) const CONVERT_MASTER_EDITION_V1_TO_V2_DISCRIMINATOR: u8 = 12; +/// Create Master Edition V3 discriminator. +pub(crate) const CREATE_MASTER_EDITION_V3_DISCRIMINATOR: u8 = 17; +/// Set Collection Size discriminator. +pub(crate) const SET_COLLECTION_SIZE_DISCRIMINATOR: u8 = 34; +/// Bubblegum Set Collection Size discriminator. +pub(crate) const BUBBLEGUM_SET_COLLECTION_SIZE_DISCRIMINATOR: u8 = 36; +/// Create Escrow Account discriminator. +pub(crate) const CREATE_ESCROW_ACCOUNT_DISCRIMINATOR: u8 = 38; +/// Close Escrow Account discriminator. +pub(crate) const CLOSE_ESCROW_ACCOUNT_DISCRIMINATOR: u8 = 39; +/// Transfer Out Of Escrow discriminator. +pub(crate) const TRANSFER_OUT_OF_ESCROW_DISCRIMINATOR: u8 = 40; +/// Collect discriminator. +pub(crate) const COLLECT_DISCRIMINATOR: u8 = 54; +/// Historical Burn NFT discriminator. +pub(crate) const BURN_NFT_DISCRIMINATOR: u8 = 29; +/// Historical Burn Edition NFT discriminator. +pub(crate) const BURN_EDITION_NFT_DISCRIMINATOR: u8 = 37; +/// Burn discriminator. +pub(crate) const BURN_DISCRIMINATOR: u8 = 41; +/// Lock discriminator. +pub(crate) const LOCK_DISCRIMINATOR: u8 = 46; +/// Unlock discriminator. +pub(crate) const UNLOCK_DISCRIMINATOR: u8 = 47; +/// Transfer discriminator. +pub(crate) const TRANSFER_DISCRIMINATOR: u8 = 49; +/// Close Accounts discriminator. +pub(crate) const CLOSE_ACCOUNTS_DISCRIMINATOR: u8 = 57; +/// Modern Create discriminator. +pub(crate) const CREATE_DISCRIMINATOR: u8 = 42; +/// Modern Mint discriminator. +pub(crate) const MINT_DISCRIMINATOR: u8 = 43; +/// Modern Print discriminator. +pub(crate) const PRINT_DISCRIMINATOR: u8 = 55; +/// Modern Update discriminator. +pub(crate) const UPDATE_DISCRIMINATOR: u8 = 50; +/// Modern Use discriminator. +pub(crate) const USE_DISCRIMINATOR: u8 = 51; +/// Modern Verify discriminator. +pub(crate) const VERIFY_DISCRIMINATOR: u8 = 52; +/// Modern Unverify discriminator. +pub(crate) const UNVERIFY_DISCRIMINATOR: u8 = 53; +/// Migrate discriminator. +pub(crate) const MIGRATE_DISCRIMINATOR: u8 = 48; +/// Resize discriminator. +pub(crate) const RESIZE_DISCRIMINATOR: u8 = 56; +/// Delegate discriminator. +pub(crate) const DELEGATE_DISCRIMINATOR: u8 = 44; +/// Revoke discriminator. +pub(crate) const REVOKE_DISCRIMINATOR: u8 = 45; +/// Current pre-release instruction inventory. +pub(crate) const INSTRUCTION_ENTRIES: &[(u8, &str, bool)] = &[ + (CREATE_METADATA_ACCOUNT_DISCRIMINATOR, "create_metadata_account", true), + ( + DEPRECATED_CREATE_MASTER_EDITION_DISCRIMINATOR, + "deprecated_create_master_edition", + true, + ), + ( + DEPRECATED_MINT_NEW_EDITION_VIA_PRINTING_TOKEN_DISCRIMINATOR, + "deprecated_mint_new_edition_from_master_edition_via_printing_token", + true, + ), + (UPDATE_METADATA_ACCOUNT_DISCRIMINATOR, "update_metadata_account", true), + ( + UPDATE_PRIMARY_SALE_HAPPENED_VIA_TOKEN_DISCRIMINATOR, + "update_primary_sale_happened_via_token", + false, + ), + ( + DEPRECATED_SET_RESERVATION_LIST_DISCRIMINATOR, + "deprecated_set_reservation_list", + true, + ), + ( + DEPRECATED_CREATE_RESERVATION_LIST_DISCRIMINATOR, + "deprecated_create_reservation_list", + true, + ), + (SIGN_METADATA_DISCRIMINATOR, "sign_metadata", false), + ( + DEPRECATED_MINT_PRINTING_TOKENS_VIA_TOKEN_DISCRIMINATOR, + "deprecated_mint_printing_tokens_via_token", + true, + ), + ( + DEPRECATED_MINT_PRINTING_TOKENS_DISCRIMINATOR, + "deprecated_mint_printing_tokens", + true, + ), + (CREATE_MASTER_EDITION_DISCRIMINATOR, "create_master_edition", true), + (PUFF_METADATA_DISCRIMINATOR, "puff_metadata", false), + ( + MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_TOKEN_DISCRIMINATOR, + "mint_new_edition_from_master_edition_via_token", + false, + ), + ( + MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_VAULT_PROXY_DISCRIMINATOR, + "mint_new_edition_from_master_edition_via_vault_proxy", + true, + ), + ( + CONVERT_MASTER_EDITION_V1_TO_V2_DISCRIMINATOR, + "convert_master_edition_v1_to_v2", + true, + ), + (UPDATE_METADATA_ACCOUNT_V2_DISCRIMINATOR, "update_metadata_account_v2", false), + (CREATE_METADATA_ACCOUNT_V2_DISCRIMINATOR, "create_metadata_account_v2", true), + (CREATE_MASTER_EDITION_V3_DISCRIMINATOR, "create_master_edition_v3", false), + (CREATE_METADATA_ACCOUNT_V3_DISCRIMINATOR, "create_metadata_account_v3", false), + (VERIFY_COLLECTION_DISCRIMINATOR, "verify_collection", false), + (UNVERIFY_COLLECTION_DISCRIMINATOR, "unverify_collection", false), + (SET_AND_VERIFY_COLLECTION_DISCRIMINATOR, "set_and_verify_collection", false), + ( + APPROVE_COLLECTION_AUTHORITY_DISCRIMINATOR, + "approve_collection_authority", + false, + ), + (REVOKE_COLLECTION_AUTHORITY_DISCRIMINATOR, "revoke_collection_authority", false), + ( + VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR, + "verify_sized_collection_item", + false, + ), + ( + UNVERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR, + "unverify_sized_collection_item", + false, + ), + ( + SET_AND_VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR, + "set_and_verify_sized_collection_item", + false, + ), + (UTILIZE_DISCRIMINATOR, "utilize", false), + (APPROVE_USE_AUTHORITY_DISCRIMINATOR, "approve_use_authority", false), + (REVOKE_USE_AUTHORITY_DISCRIMINATOR, "revoke_use_authority", false), + (FREEZE_DELEGATED_ACCOUNT_DISCRIMINATOR, "freeze_delegated_account", true), + (THAW_DELEGATED_ACCOUNT_DISCRIMINATOR, "thaw_delegated_account", true), + (REMOVE_CREATOR_VERIFICATION_DISCRIMINATOR, "remove_creator_verification", false), + (SET_TOKEN_STANDARD_DISCRIMINATOR, "set_token_standard", false), + (SET_COLLECTION_SIZE_DISCRIMINATOR, "set_collection_size", false), + ( + BUBBLEGUM_SET_COLLECTION_SIZE_DISCRIMINATOR, + "bubblegum_set_collection_size", + false, + ), + (CREATE_ESCROW_ACCOUNT_DISCRIMINATOR, "create_escrow_account", false), + (CLOSE_ESCROW_ACCOUNT_DISCRIMINATOR, "close_escrow_account", false), + (TRANSFER_OUT_OF_ESCROW_DISCRIMINATOR, "transfer_out_of_escrow", false), + (BURN_NFT_DISCRIMINATOR, "burn_nft", true), + (BURN_EDITION_NFT_DISCRIMINATOR, "burn_edition_nft", true), + (BURN_DISCRIMINATOR, "burn", false), + (CREATE_DISCRIMINATOR, "create", false), + (MINT_DISCRIMINATOR, "mint", false), + (LOCK_DISCRIMINATOR, "lock", false), + (UNLOCK_DISCRIMINATOR, "unlock", false), + (TRANSFER_DISCRIMINATOR, "transfer", false), + (COLLECT_DISCRIMINATOR, "collect", false), + (PRINT_DISCRIMINATOR, "print", false), + (UPDATE_DISCRIMINATOR, "update", false), + (USE_DISCRIMINATOR, "use", false), + (VERIFY_DISCRIMINATOR, "verify", false), + (UNVERIFY_DISCRIMINATOR, "unverify", false), + (MIGRATE_DISCRIMINATOR, "migrate", false), + (RESIZE_DISCRIMINATOR, "resize", false), + (CLOSE_ACCOUNTS_DISCRIMINATOR, "close_accounts", false), + (DELEGATE_DISCRIMINATOR, "delegate", false), + (REVOKE_DISCRIMINATOR, "revoke", false), +]; + +/// Canonical tracing target for this decoder. +pub(crate) const TRACING_TARGET: &str = "kb-lib.decoder.metadata.metaplex_token_metadata"; diff --git a/kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs b/kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs new file mode 100644 index 0000000..abd637f --- /dev/null +++ b/kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs @@ -0,0 +1,3152 @@ +// file: kb-lib/src/decoder/metadata/metaplex_token_metadata/decoder.rs +// version: 28 + +//! Exact contextual dispatch for Metaplex Token Metadata. + +const SURFACES: &[crate::DecoderSurface] = &[crate::DecoderSurface { + program_id: kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + surface_code: crate::decoder::metadata::metaplex_token_metadata::SURFACE_CODE, + priority: 100, +}]; + +const PROGRAM_IDS: &[&str] = &[kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID]; + +/// Exact decoder for the Metaplex Token Metadata program. +#[derive(Clone, Debug, Default)] +pub struct MetadataMetaplexTokenMetadataDecoder; + +impl crate::ProtocolDecoder + for crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder +{ + fn decoder_name(&self) -> &'static str { + return "kb_decoder_metadata_metaplex_token_metadata"; + } + + fn decoder_version(&self) -> &'static str { + return env!("CARGO_PKG_VERSION"); + } + + fn program_ids(&self) -> &'static [&'static str] { + return PROGRAM_IDS; + } + + fn supports_observation( + &self, + observation: &crate::ProgramObservation, + ) -> crate::DecoderSupport { + return if crate::ProtocolDecoder::handles_program_id(self, &observation.program_id) { + crate::DecoderSupport::Yes + } else { + crate::DecoderSupport::No + }; + } + + fn decode_observation( + &self, + _observation: &crate::ProgramObservation, + ) -> kb_core::Result> { + return std::result::Result::Ok(std::vec::Vec::new()); + } +} + +impl crate::InstructionDecoder + for crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder +{ + fn identity(&self) -> crate::DecoderIdentity { + return crate::DecoderIdentity { + name: crate::decoder::metadata::metaplex_token_metadata::SURFACE_CODE.to_string(), + version: env!("CARGO_PKG_VERSION").to_string(), + }; + } + + fn surfaces(&self) -> &'static [crate::DecoderSurface] { + return SURFACES; + } + + fn coverage(&self) -> std::vec::Vec { + return crate::decoder::metadata::metaplex_token_metadata::INSTRUCTION_ENTRIES + .iter() + .map(|(discriminator, code, historical)| { + return crate::DecoderCoverageDeclaration { + program_id: kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID + .to_string(), + surface_code: std::option::Option::Some( + crate::decoder::metadata::metaplex_token_metadata::SURFACE_CODE.to_string(), + ), + entry_kind: crate::DecoderCoverageEntryKind::Instruction, + entry_code: (*code).to_string(), + discriminator_hex: std::option::Option::Some(format!("{discriminator:02x}")), + historical: *historical, + }; + }) + .collect(); + } + + fn recognize(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderRecognition { + if input.program_id != kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID { + return crate::DecoderRecognition::incompatible(); + } + let discriminator = + crate::decoder::metadata::metaplex_token_metadata::payload_discriminator(input); + let entry = discriminator + .and_then(crate::decoder::metadata::metaplex_token_metadata::entry_for_discriminator); + return crate::DecoderRecognition::compatible( + entry.is_some(), + 100, + std::option::Option::Some( + crate::decoder::metadata::metaplex_token_metadata::SURFACE_CODE.to_string(), + ), + entry.map(|(_, code, _)| return code.to_string()), + discriminator.map(|value| return format!("{value:02x}")), + ); + } + + fn decode(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult { + if input.program_id != kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID { + return crate::DecoderExecutionResult::unsupported(std::option::Option::None); + } + let result = crate::decoder::metadata::metaplex_token_metadata::decode(input); + if matches!( + result.status, + crate::DecoderOutcomeStatus::Failed | crate::DecoderOutcomeStatus::Unsupported + ) { + tracing::error!( + target: crate::decoder::metadata::metaplex_token_metadata::TRACING_TARGET, + action = "decode_failure", + signature = %input.signature, + slot = input.slot, + instruction_path = %input.instruction_path, + program_id = %input.program_id, + processor_name = crate::decoder::metadata::metaplex_token_metadata::SURFACE_CODE, + processor_version = env!("CARGO_PKG_VERSION"), + input_key = %input.replay_input_key, + result_status = ?result.status, + diagnostics = ?result.diagnostics, + "Metaplex Token Metadata instruction was not decoded successfully" + ); + } + tracing::debug!( + target: crate::decoder::metadata::metaplex_token_metadata::TRACING_TARGET, + action = "decode", + signature = %input.signature, + instruction_path = %input.instruction_path, + transaction_failed = input.transaction_failed, + result_status = ?result.status, + observation_count = result.observations.len(), + "Metaplex Token Metadata instruction decode completed" + ); + return result; + } +} + +#[cfg(test)] +mod tests { + use base64::Engine; // rust-rules: trait-import + + const AUDIT_MATRIX_JSON: &str = + include_str!("../../../../../docs/METAPLEX_TOKEN_METADATA_MATRIX.json"); + const MAX_SDK_INSTRUCTION_MODULES: usize = 128; + const MIN_SDK_INSTRUCTION_MODULES: usize = 90; + const MIN_PDA_FAMILIES: usize = 8; + const MIN_ACCOUNT_LAYOUTS: usize = 9; + + fn input( + payload: &[u8], + account_count: usize, + failed: bool, + path: &str, + ) -> crate::CoreInstructionReplayInput { + let keys = (0..account_count).map(|position| return serde_json::json!({ + "accountIndex": position, + "accountKey": format!("account{position}"), + "source": "static", + "signer": match position { 2 | 3 => true, 1 if account_count == 2 => true, _ => false }, + "writable": match position { 0 | 3 => true, _ => false }, + })).collect::>(); + let accounts = (0..account_count) + .map(|position| { + return serde_json::json!({ + "accountIndex": position, + "accountKey": format!("account{position}"), + }); + }) + .collect::>(); + let result = crate::CoreInstructionReplayInput::new( + format!("signature:{path}"), + "signature", + 42, + path, + kb_program_ids::METADATA_METAPLEX_TOKEN_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(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!("Metaplex replay input failed: {error}"), + }; + } + + fn input_with_flags( + payload: &[u8], + flags: &[(bool, bool)], + failed: bool, + path: &str, + ) -> crate::CoreInstructionReplayInput { + let keys = flags + .iter() + .enumerate() + .map(|(position, (signer, writable))| { + return serde_json::json!({ + "accountIndex": position, + "accountKey": format!("account{position}"), + "source": "static", + "signer": signer, + "writable": writable, + }); + }) + .collect::>(); + let accounts = flags + .iter() + .enumerate() + .map(|(position, _)| { + return serde_json::json!({ + "accountIndex": position, + "accountKey": format!("account{position}"), + }); + }) + .collect::>(); + let result = crate::CoreInstructionReplayInput::new( + format!("signature:{path}"), + "signature", + 42, + path, + kb_program_ids::METADATA_METAPLEX_TOKEN_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(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!("Metaplex replay input failed: {error}") + }, + }; + } + + fn data_v2() -> mpl_token_metadata::types::DataV2 { + return mpl_token_metadata::types::DataV2 { + name: "Khadhroony".to_string(), + symbol: "KB".to_string(), + uri: "https://example.invalid/metadata.json".to_string(), + seller_fee_basis_points: 500, + creators: std::option::Option::None, + collection: std::option::Option::None, + uses: std::option::Option::None, + }; + } + + fn create_wire() -> std::vec::Vec { + let args = mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs { + data: data_v2(), + is_mutable: true, + collection_details: std::option::Option::None, + }; + let mut wire = std::vec![crate::decoder::metadata::metaplex_token_metadata::CREATE_METADATA_ACCOUNT_V3_DISCRIMINATOR]; + let encoded = borsh_0_10::to_vec(&args); + match encoded { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => { + panic!("CreateMetadataAccountV3 encode failed: {error}") + }, + } + return wire; + } + + fn update_wire() -> std::vec::Vec { + let args = mpl_token_metadata::instructions::UpdateMetadataAccountV2InstructionArgs { + data: std::option::Option::Some(data_v2()), + new_update_authority: std::option::Option::None, + primary_sale_happened: std::option::Option::Some(true), + is_mutable: std::option::Option::Some(false), + }; + let mut wire = std::vec![crate::decoder::metadata::metaplex_token_metadata::UPDATE_METADATA_ACCOUNT_V2_DISCRIMINATOR]; + let encoded = borsh_0_10::to_vec(&args); + match encoded { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => { + panic!("UpdateMetadataAccountV2 encode failed: {error}") + }, + } + return wire; + } + + fn approve_use_authority_wire(number_of_uses: u64) -> std::vec::Vec { + let args = + mpl_token_metadata::instructions::ApproveUseAuthorityInstructionArgs { number_of_uses }; + let mut wire = std::vec![ + crate::decoder::metadata::metaplex_token_metadata::APPROVE_USE_AUTHORITY_DISCRIMINATOR + ]; + let encoded = borsh_0_10::to_vec(&args); + match encoded { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => panic!("ApproveUseAuthority encode failed: {error}"), + } + return wire; + } + + fn utilize_wire(number_of_uses: u64) -> std::vec::Vec { + let args = mpl_token_metadata::instructions::UtilizeInstructionArgs { number_of_uses }; + let mut wire = + std::vec![crate::decoder::metadata::metaplex_token_metadata::UTILIZE_DISCRIMINATOR]; + let encoded = borsh_0_10::to_vec(&args); + match encoded { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => panic!("Utilize encode failed: {error}"), + } + return wire; + } + + fn historical_data() -> mpl_token_metadata::types::Data { + return mpl_token_metadata::types::Data { + name: "Legacy V1".to_string(), + symbol: "KV1".to_string(), + uri: "https://example.invalid/v1.json".to_string(), + seller_fee_basis_points: 125, + creators: std::option::Option::None, + }; + } + + fn historical_create_v1_wire() -> std::vec::Vec { + let mut wire = std::vec![crate::decoder::metadata::metaplex_token_metadata::CREATE_METADATA_ACCOUNT_DISCRIMINATOR]; + match borsh_0_10::to_vec(&historical_data()) { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => { + panic!("CreateMetadataAccount data encode failed: {error}") + }, + } + match borsh_0_10::to_vec(&true) { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => { + panic!("CreateMetadataAccount mutable encode failed: {error}") + }, + } + return wire; + } + + fn historical_update_v1_wire() -> std::vec::Vec { + let mut wire = std::vec![crate::decoder::metadata::metaplex_token_metadata::UPDATE_METADATA_ACCOUNT_DISCRIMINATOR]; + let data = std::option::Option::Some(historical_data()); + let authority = std::option::Option::Some([7_u8; 32]); + let primary_sale = std::option::Option::Some(true); + for encoded in [ + borsh_0_10::to_vec(&data), + borsh_0_10::to_vec(&authority), + borsh_0_10::to_vec(&primary_sale), + ] { + match encoded { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => { + panic!("UpdateMetadataAccount encode failed: {error}") + }, + } + } + return wire; + } + + fn historical_create_v2_wire() -> std::vec::Vec { + let data = mpl_token_metadata::types::DataV2 { + name: "Legacy Khadhroony".to_string(), + symbol: "KLEG".to_string(), + uri: "https://example.invalid/legacy.json".to_string(), + seller_fee_basis_points: 250, + creators: std::option::Option::None, + collection: std::option::Option::None, + uses: std::option::Option::None, + }; + let mut wire = std::vec![crate::decoder::metadata::metaplex_token_metadata::CREATE_METADATA_ACCOUNT_V2_DISCRIMINATOR]; + let encoded_data = borsh_0_10::to_vec(&data); + match encoded_data { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => { + panic!("CreateMetadataAccountV2 data encode failed: {error}") + }, + } + let encoded_is_mutable = borsh_0_10::to_vec(&true); + match encoded_is_mutable { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => { + panic!("CreateMetadataAccountV2 is_mutable encode failed: {error}") + }, + } + return wire; + } + + fn mint_new_edition_wire(discriminator: u8, edition: u64) -> std::vec::Vec { + let mut wire = std::vec![discriminator]; + let args = + mpl_token_metadata::types::MintNewEditionFromMasterEditionViaTokenArgs { edition }; + let encoded = borsh_0_10::to_vec(&args); + match encoded { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => { + panic!("MintNewEditionFromMasterEdition args encode failed: {error}") + }, + } + return wire; + } + + fn create_master_edition_v3_wire(max_supply: std::option::Option) -> std::vec::Vec { + let mut wire = std::vec![crate::decoder::metadata::metaplex_token_metadata::CREATE_MASTER_EDITION_V3_DISCRIMINATOR]; + let encoded = borsh_0_10::to_vec(&max_supply); + match encoded { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => { + panic!("CreateMasterEditionV3 max_supply encode failed: {error}") + }, + } + return wire; + } + + fn set_collection_size_wire(discriminator: u8, size: u64) -> std::vec::Vec { + let mut wire = std::vec![discriminator]; + let args = mpl_token_metadata::types::SetCollectionSizeArgs { size }; + let encoded = borsh_0_10::to_vec(&args); + match encoded { + std::result::Result::Ok(mut value) => wire.append(&mut value), + std::result::Result::Err(error) => { + panic!("SetCollectionSize args encode failed: {error}") + }, + } + return wire; + } + + fn programmable_wire( + discriminator: u8, + args: &T, + ) -> std::vec::Vec { + let mut wire = std::vec![discriminator]; + let encoded = match borsh_0_10::BorshSerialize::try_to_vec(args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("programmable fixture serialization failed: {error}") + }, + }; + wire.extend_from_slice(encoded.as_slice()); + return wire; + } + + fn delegate_wire(args: &mpl_token_metadata::types::DelegateArgs) -> std::vec::Vec { + let mut wire = + std::vec![crate::decoder::metadata::metaplex_token_metadata::DELEGATE_DISCRIMINATOR]; + let encoded = match borsh_0_10::BorshSerialize::try_to_vec(args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("delegate fixture serialization failed: {error}") + }, + }; + wire.extend_from_slice(encoded.as_slice()); + return wire; + } + + fn revoke_wire(args: &mpl_token_metadata::types::RevokeArgs) -> std::vec::Vec { + let mut wire = + std::vec![crate::decoder::metadata::metaplex_token_metadata::REVOKE_DISCRIMINATOR]; + let encoded = match borsh_0_10::BorshSerialize::try_to_vec(args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + panic!("revoke fixture serialization failed: {error}") + }, + }; + wire.extend_from_slice(encoded.as_slice()); + return wire; + } + + #[test] + fn decoder_identity_program_boundary_and_coverage_are_exact() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + assert_eq!( + crate::ProtocolDecoder::decoder_name(&decoder), + "kb_decoder_metadata_metaplex_token_metadata" + ); + assert_eq!( + crate::ProtocolDecoder::program_ids(&decoder), + &[kb_program_ids::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID] + ); + assert_eq!( + crate::decoder::metadata::metaplex_token_metadata::TRACING_TARGET, + "kb-lib.decoder.metadata.metaplex_token_metadata" + ); + assert_eq!(crate::InstructionDecoder::coverage(&decoder).len(), 58); + } + + #[test] + fn printed_edition_mint_variants_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let token = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + mint_new_edition_wire( + crate::decoder::metadata::metaplex_token_metadata::MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_TOKEN_DISCRIMINATOR, + u64::MAX, + ) + .as_slice(), + &[ + (false, true), + (false, true), + (false, true), + (false, true), + (false, true), + (true, false), + (true, true), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "printed_edition_token", + ), + ); + assert_eq!(token.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + token.recognized_entry_code.as_deref(), + std::option::Option::Some("mint_new_edition_from_master_edition_via_token") + ); + assert_eq!(token.observations[0].payload_json["parameters"]["edition"], u64::MAX); + assert_eq!(token.observations[0].payload_json["accounts"][4]["role"], "edition_marker"); + let vault = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + mint_new_edition_wire( + crate::decoder::metadata::metaplex_token_metadata::MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_VAULT_PROXY_DISCRIMINATOR, + 248, + ) + .as_slice(), + &[ + (false, true), + (false, true), + (false, true), + (false, true), + (false, true), + (true, false), + (true, true), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + true, + "printed_edition_vault", + ), + ); + assert_eq!(vault.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + vault.recognized_entry_code.as_deref(), + std::option::Option::Some("mint_new_edition_from_master_edition_via_vault_proxy") + ); + assert_eq!(vault.observations[0].payload_json["parameters"]["edition"], 248); + assert_eq!( + vault.observations[0].payload_json["accounts"][14]["role"], + "token_vault_program" + ); + assert!(!vault.observations[0].observation_committed); + } + + fn modern_create_wire() -> std::vec::Vec { + let args = mpl_token_metadata::types::CreateArgs::V1 { + name: "Modern Create".to_string(), + symbol: "MCR".to_string(), + uri: "https://example.invalid/modern-create.json".to_string(), + seller_fee_basis_points: 500, + creators: std::option::Option::None, + primary_sale_happened: false, + is_mutable: true, + token_standard: mpl_token_metadata::types::TokenStandard::NonFungible, + collection: std::option::Option::None, + uses: std::option::Option::None, + collection_details: std::option::Option::None, + rule_set: std::option::Option::None, + decimals: std::option::Option::None, + print_supply: std::option::Option::None, + }; + return programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::CREATE_DISCRIMINATOR, + &args, + ); + } + + #[test] + fn modern_create_decodes_exact_contract() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let result = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + modern_create_wire().as_slice(), + &[ + (false, true), + (false, true), + (true, true), + (true, false), + (true, true), + (true, false), + (false, false), + (false, false), + (false, false), + ], + false, + "modern_create", + ), + ); + assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some("create")); + assert_eq!(result.observations[0].payload_json["accounts"][0]["role"], "metadata"); + assert_eq!( + result.observations[0].payload_json["parameters"]["V1"]["name"], + "Modern Create" + ); + } + + #[test] + fn modern_create_rejects_truncated_invalid_and_suffix_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::CREATE_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (true, true), + (true, false), + (true, true), + (true, false), + (false, false), + (false, false), + (false, false), + ], + false, + "modern_create_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + modern_create_wire().as_slice(), + &[ + (false, true), + (false, true), + (false, true), + (true, false), + (true, true), + (true, false), + (false, false), + (false, false), + (false, false), + ], + false, + "modern_create_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let mut suffix_wire = modern_create_wire(); + suffix_wire.push(1); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + suffix_wire.as_slice(), + &[ + (false, true), + (false, true), + (true, true), + (true, false), + (true, true), + (true, false), + (false, false), + (false, false), + (false, false), + ], + false, + "modern_create_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + fn modern_mint_wire(amount: u64) -> std::vec::Vec { + let args = mpl_token_metadata::types::MintArgs::V1 { + amount, + authorization_data: std::option::Option::None, + }; + return programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::MINT_DISCRIMINATOR, + &args, + ); + } + + #[test] + fn modern_mint_decodes_exact_contract() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let result = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + modern_mint_wire(u64::MAX).as_slice(), + &[ + (false, true), + (false, false), + (false, false), + (false, true), + (false, true), + (false, true), + (true, false), + (false, false), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "modern_mint", + ), + ); + assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some("mint")); + assert_eq!(result.observations[0].payload_json["parameters"]["V1"]["amount"], u64::MAX); + assert_eq!(result.observations[0].payload_json["accounts"][4]["role"], "token_record"); + assert_eq!( + result.observations[0].payload_json["accounts"][14]["role"], + "authorization_rules" + ); + } + + #[test] + fn modern_mint_rejects_truncated_invalid_and_suffix_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let flags = [ + (false, true), + (false, false), + (false, false), + (false, true), + (false, true), + (false, true), + (true, false), + (false, false), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::MINT_DISCRIMINATOR], + &flags, + false, + "modern_mint_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let mut invalid_flags = flags; + invalid_flags[6] = (false, false); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + modern_mint_wire(1).as_slice(), + &invalid_flags, + false, + "modern_mint_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let mut suffix_wire = modern_mint_wire(2); + suffix_wire.push(1); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags(suffix_wire.as_slice(), &flags, false, "modern_mint_suffix"), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + fn modern_print_wire_v1(edition: u64) -> std::vec::Vec { + let args = mpl_token_metadata::types::PrintArgs::V1 { edition }; + return programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::PRINT_DISCRIMINATOR, + &args, + ); + } + + fn modern_print_wire_v2(edition: u64) -> std::vec::Vec { + let args = mpl_token_metadata::types::PrintArgs::V2 { edition }; + return programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::PRINT_DISCRIMINATOR, + &args, + ); + } + + #[test] + fn modern_print_v1_and_v2_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let flags = [ + (false, true), + (false, true), + (true, true), + (false, false), + (false, true), + (true, false), + (false, true), + (false, true), + (false, true), + (true, true), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let v1 = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + modern_print_wire_v1(u64::MAX).as_slice(), + &flags, + false, + "modern_print_v1", + ), + ); + assert_eq!(v1.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(v1.recognized_entry_code.as_deref(), std::option::Option::Some("print")); + assert_eq!(v1.observations[0].payload_json["parameters"]["V1"]["edition"], u64::MAX); + assert_eq!(v1.observations[0].payload_json["accounts"][8]["role"], "edition_marker_pda"); + let v2 = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags(modern_print_wire_v2(42).as_slice(), &flags, true, "modern_print_v2"), + ); + assert_eq!(v2.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(v2.observations[0].payload_json["parameters"]["V2"]["edition"], 42); + assert!(!v2.observations[0].observation_committed); + } + + #[test] + fn modern_print_rejects_truncated_invalid_and_suffix_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let flags = [ + (false, true), + (false, true), + (true, true), + (false, false), + (false, true), + (true, false), + (false, true), + (false, true), + (false, true), + (true, true), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::PRINT_DISCRIMINATOR], + &flags, + false, + "modern_print_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let mut invalid_flags = flags; + invalid_flags[10] = (false, false); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + modern_print_wire_v1(1).as_slice(), + &invalid_flags, + false, + "modern_print_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let mut suffix_wire = modern_print_wire_v2(2); + suffix_wire.push(1); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags(suffix_wire.as_slice(), &flags, false, "modern_print_suffix"), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + fn modern_update_wire() -> std::vec::Vec { + let args = mpl_token_metadata::types::UpdateArgs::AsCollectionDelegateV2 { + collection: mpl_token_metadata::types::CollectionToggle::None, + authorization_data: std::option::Option::None, + }; + return programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::UPDATE_DISCRIMINATOR, + &args, + ); + } + + #[test] + fn modern_update_variants_decode_exact_contract() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let flags = [ + (true, false), + (false, false), + (false, false), + (false, false), + (false, true), + (false, false), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let result = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags(modern_update_wire().as_slice(), &flags, false, "modern_update"), + ); + assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some("update")); + assert_eq!( + result.observations[0].payload_json["parameters"]["AsCollectionDelegateV2"]["collection"], + "None" + ); + assert_eq!(result.observations[0].payload_json["accounts"][4]["role"], "metadata"); + assert_eq!( + result.observations[0].payload_json["accounts"][10]["role"], + "authorization_rules" + ); + } + + #[test] + fn modern_update_rejects_truncated_invalid_and_suffix_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let flags = [ + (true, false), + (false, false), + (false, false), + (false, false), + (false, true), + (false, false), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::UPDATE_DISCRIMINATOR], + &flags, + false, + "modern_update_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let mut invalid_flags = flags; + invalid_flags[0] = (false, false); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + modern_update_wire().as_slice(), + &invalid_flags, + false, + "modern_update_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let mut suffix_wire = modern_update_wire(); + suffix_wire.push(1); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags(suffix_wire.as_slice(), &flags, false, "modern_update_suffix"), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + fn modern_use_wire() -> std::vec::Vec { + let args = mpl_token_metadata::types::UseArgs::V1 { + authorization_data: std::option::Option::None, + }; + return programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::USE_DISCRIMINATOR, + &args, + ); + } + + fn modern_verification_wire( + discriminator: u8, + args: mpl_token_metadata::types::VerificationArgs, + ) -> std::vec::Vec { + return programmable_wire(discriminator, &args); + } + + #[test] + fn modern_use_verify_and_unverify_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let use_flags = [ + (true, false), + (false, true), + (false, true), + (false, false), + (false, true), + (false, true), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let use_result = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags(modern_use_wire().as_slice(), &use_flags, false, "modern_use"), + ); + assert_eq!(use_result.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(use_result.recognized_entry_code.as_deref(), std::option::Option::Some("use")); + assert_eq!( + use_result.observations[0].payload_json["parameters"]["V1"]["authorization_data"], + serde_json::Value::Null + ); + let verify_flags = [ + (true, false), + (false, false), + (false, true), + (false, false), + (false, true), + (false, false), + (false, false), + (false, false), + ]; + let verify = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + modern_verification_wire( + crate::decoder::metadata::metaplex_token_metadata::VERIFY_DISCRIMINATOR, + mpl_token_metadata::types::VerificationArgs::CollectionV1, + ) + .as_slice(), + &verify_flags, + false, + "modern_verify", + ), + ); + assert_eq!(verify.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(verify.observations[0].payload_json["parameters"], "CollectionV1"); + let unverify = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + modern_verification_wire( + crate::decoder::metadata::metaplex_token_metadata::UNVERIFY_DISCRIMINATOR, + mpl_token_metadata::types::VerificationArgs::CreatorV1, + ) + .as_slice(), + &verify_flags, + true, + "modern_unverify", + ), + ); + assert_eq!(unverify.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + unverify.recognized_entry_code.as_deref(), + std::option::Option::Some("unverify") + ); + assert!(!unverify.observations[0].observation_committed); + } + + #[test] + fn modern_use_verify_and_unverify_reject_invalid_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let use_flags = [ + (true, false), + (false, true), + (false, true), + (false, false), + (false, true), + (false, true), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::USE_DISCRIMINATOR], + &use_flags, + false, + "modern_use_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let mut invalid_flags = use_flags; + invalid_flags[0] = (false, false); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + modern_use_wire().as_slice(), + &invalid_flags, + false, + "modern_use_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let verify_flags = [ + (true, false), + (false, false), + (false, true), + (false, false), + (false, true), + (false, false), + (false, false), + (false, false), + ]; + let mut suffix_wire = modern_verification_wire( + crate::decoder::metadata::metaplex_token_metadata::VERIFY_DISCRIMINATOR, + mpl_token_metadata::types::VerificationArgs::CreatorV1, + ); + suffix_wire.push(1); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags(suffix_wire.as_slice(), &verify_flags, false, "modern_verify_suffix"), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn printed_edition_mint_variants_reject_truncated_wire_invalid_flags_and_suffixes() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_TOKEN_DISCRIMINATOR], + &[(false, true); 13], + false, + "printed_edition_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + mint_new_edition_wire( + crate::decoder::metadata::metaplex_token_metadata::MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_TOKEN_DISCRIMINATOR, + 1, + ) + .as_slice(), + &[ + (false, true), + (false, true), + (false, true), + (false, true), + (false, true), + (false, false), + (true, true), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "printed_edition_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let mut suffix_wire = mint_new_edition_wire( + crate::decoder::metadata::metaplex_token_metadata::MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_VAULT_PROXY_DISCRIMINATOR, + 2, + ); + suffix_wire.push(1); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + suffix_wire.as_slice(), + &[ + (false, true), + (false, true), + (false, true), + (false, true), + (false, true), + (true, false), + (true, true), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "printed_edition_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn master_edition_creation_and_conversion_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let create = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + create_master_edition_v3_wire(std::option::Option::Some(u64::MAX)).as_slice(), + &[ + (false, true), + (false, true), + (true, false), + (true, false), + (true, true), + (false, true), + (false, false), + (false, false), + ], + false, + "master_edition_create", + ), + ); + assert_eq!(create.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + create.recognized_entry_code.as_deref(), + std::option::Option::Some("create_master_edition_v3") + ); + assert_eq!(create.observations[0].payload_json["parameters"]["max_supply"], u64::MAX); + assert_eq!(create.observations[0].payload_json["accounts"][0]["role"], "master_edition"); + let convert = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::CONVERT_MASTER_EDITION_V1_TO_V2_DISCRIMINATOR], + &[(false, true), (false, true), (false, true)], + true, + "master_edition_convert", + ), + ); + assert_eq!(convert.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + convert.recognized_entry_code.as_deref(), + std::option::Option::Some("convert_master_edition_v1_to_v2") + ); + assert!(!convert.observations[0].observation_committed); + } + + #[test] + fn master_edition_group_rejects_truncated_wire_invalid_flags_and_suffixes() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::CREATE_MASTER_EDITION_V3_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (true, false), + (true, false), + (true, true), + (false, true), + (false, false), + (false, false), + ], + false, + "master_edition_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + create_master_edition_v3_wire(std::option::Option::None).as_slice(), + &[ + (false, true), + (false, true), + (false, false), + (true, false), + (true, true), + (false, true), + (false, false), + (false, false), + ], + false, + "master_edition_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::CONVERT_MASTER_EDITION_V1_TO_V2_DISCRIMINATOR, 1], + &[(false, true), (false, true), (false, true)], + false, + "master_edition_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn historical_v1_create_and_update_decode_exact_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let create = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + historical_create_v1_wire().as_slice(), + &[ + (false, true), + (false, false), + (true, false), + (true, true), + (false, false), + (false, false), + (false, false), + ], + false, + "legacy_create_v1", + ), + ); + assert_eq!(create.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + create.recognized_entry_code.as_deref(), + std::option::Option::Some("create_metadata_account") + ); + assert_eq!(create.observations[0].payload_json["parameters"]["data"]["name"], "Legacy V1"); + let update = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + historical_update_v1_wire().as_slice(), + &[(false, true), (true, false)], + true, + "legacy_update_v1", + ), + ); + assert_eq!(update.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + update.recognized_entry_code.as_deref(), + std::option::Option::Some("update_metadata_account") + ); + assert_eq!( + update.observations[0].payload_json["parameters"]["primary_sale_happened"], + true + ); + assert!(!update.observations[0].observation_committed); + } + + #[test] + fn historical_v1_wires_reject_truncation_and_invalid_accounts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::UPDATE_METADATA_ACCOUNT_DISCRIMINATOR], + &[(false, true), (true, false)], + false, + "legacy_update_v1_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + historical_create_v1_wire().as_slice(), + &[ + (false, true), + (false, false), + (false, false), + (true, true), + (false, false), + (false, false), + (false, false), + ], + false, + "legacy_create_v1_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn historical_create_metadata_account_v2_decodes_bounded_data_v2() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let result = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + historical_create_v2_wire().as_slice(), + &[ + (false, true), + (false, false), + (true, false), + (true, true), + (false, false), + (false, false), + (false, false), + ], + false, + "legacy_create_v2", + ), + ); + assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + result.recognized_entry_code.as_deref(), + std::option::Option::Some("create_metadata_account_v2") + ); + assert_eq!( + result.observations[0].payload_json["parameters"]["data"]["name"], + "Legacy Khadhroony" + ); + assert_eq!(result.observations[0].payload_json["accounts"][2]["role"], "mint_authority"); + } + + #[test] + fn historical_create_metadata_account_v2_rejects_truncated_wire_and_invalid_flags() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::CREATE_METADATA_ACCOUNT_V2_DISCRIMINATOR], + &[ + (false, true), + (false, false), + (true, false), + (true, true), + (false, false), + (false, false), + ], + false, + "legacy_create_v2_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + historical_create_v2_wire().as_slice(), + &[ + (false, true), + (false, false), + (false, false), + (true, true), + (false, false), + (false, false), + ], + false, + "legacy_create_v2_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn create_metadata_account_v3_decodes_official_borsh_wire() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let result = crate::InstructionDecoder::decode( + &decoder, + &input(create_wire().as_slice(), 6, false, "0"), + ); + assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + result.recognized_entry_code.as_deref(), + std::option::Option::Some("create_metadata_account_v3") + ); + assert_eq!(result.observations.len(), 1); + assert_eq!(result.observations[0].payload_json["parameters"]["data"]["name"], "Khadhroony"); + assert!(result.observations[0].observation_committed); + } + + #[test] + fn update_metadata_account_v2_preserves_failed_transaction_as_uncommitted() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let result = crate::InstructionDecoder::decode( + &decoder, + &input(update_wire().as_slice(), 2, true, "1/0"), + ); + assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + result.observations[0].event.source_kind, + crate::EventSourceKind::InnerInstruction + ); + assert!(!result.observations[0].observation_committed); + assert_eq!( + result.observations[0].payload_json["parameters"]["primary_sale_happened"], + true + ); + } + + #[test] + fn collection_verification_family_decodes_exact_account_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let verify = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::VERIFY_COLLECTION_DISCRIMINATOR], + &[ + (false, true), + (true, true), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "2/0", + ), + ); + assert_eq!(verify.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + verify.recognized_entry_code.as_deref(), + std::option::Option::Some("verify_collection") + ); + assert_eq!( + verify.observations[0].payload_json["accounts"][6]["role"], + "collection_authority_record" + ); + let unverify = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::UNVERIFY_COLLECTION_DISCRIMINATOR], + &[(false, true), (true, true), (false, false), (false, false), (false, false)], + true, + "2/1", + ), + ); + assert_eq!(unverify.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!unverify.observations[0].observation_committed); + let set_and_verify = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::SET_AND_VERIFY_COLLECTION_DISCRIMINATOR], + &[ + (false, true), + (true, true), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "3", + ), + ); + assert_eq!(set_and_verify.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + set_and_verify.observations[0].payload_json["accounts"][3]["role"], + "update_authority" + ); + } + + #[test] + fn collection_authority_administration_decodes_exact_account_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let approve = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::APPROVE_COLLECTION_AUTHORITY_DISCRIMINATOR], + &[ + (false, true), + (false, false), + (true, true), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "approve_collection_authority", + ), + ); + assert_eq!(approve.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + approve.recognized_entry_code.as_deref(), + std::option::Option::Some("approve_collection_authority") + ); + assert_eq!( + approve.observations[0].payload_json["accounts"][0]["role"], + "collection_authority_record" + ); + assert_eq!( + approve.observations[0].payload_json["accounts"][1]["role"], + "new_collection_authority" + ); + assert_eq!(approve.observations[0].payload_json["accounts"][7]["role"], "rent"); + let revoke = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::REVOKE_COLLECTION_AUTHORITY_DISCRIMINATOR], + &[(false, true), (false, true), (true, true), (false, false), (false, false)], + true, + "4/0", + ), + ); + assert_eq!(revoke.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!revoke.observations[0].observation_committed); + assert_eq!( + revoke.observations[0].payload_json["accounts"][1]["role"], + "delegate_authority" + ); + assert_eq!(revoke.observations[0].payload_json["accounts"][2]["role"], "revoke_authority"); + } + + #[test] + fn collection_authority_administration_rejects_invalid_flags_and_suffixes() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::APPROVE_COLLECTION_AUTHORITY_DISCRIMINATOR], + &[ + (false, true), + (false, false), + (true, false), + (true, true), + (false, false), + (false, false), + (false, false), + ], + false, + "approve-invalid-flags", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::REVOKE_COLLECTION_AUTHORITY_DISCRIMINATOR, 0], + &[(false, true), (false, true), (true, true), (false, false), (false, false)], + false, + "revoke-suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn use_authority_family_decodes_parameters_and_exact_account_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let approve = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + approve_use_authority_wire(u64::MAX).as_slice(), + &[ + (false, true), + (true, true), + (true, true), + (false, false), + (false, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "approve_use_authority", + ), + ); + assert_eq!(approve.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(approve.observations[0].payload_json["parameters"]["number_of_uses"], u64::MAX); + assert_eq!( + approve.observations[0].payload_json["accounts"][0]["role"], + "use_authority_record" + ); + assert_eq!(approve.observations[0].payload_json["accounts"][7]["role"], "burner"); + assert_eq!(approve.observations[0].payload_json["accounts"][10]["role"], "rent"); + let revoke = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::REVOKE_USE_AUTHORITY_DISCRIMINATOR], + &[ + (false, true), + (true, true), + (false, false), + (false, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + true, + "revoke_use_authority/0", + ), + ); + assert_eq!(revoke.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!revoke.observations[0].observation_committed); + assert_eq!( + revoke.observations[0].payload_json["accounts"][3]["role"], + "owner_token_account" + ); + let utilize = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + utilize_wire(3).as_slice(), + &[ + (false, true), + (false, true), + (false, true), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, true), + (false, false), + ], + false, + "utilize", + ), + ); + assert_eq!(utilize.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(utilize.observations[0].payload_json["parameters"]["number_of_uses"], 3); + assert_eq!( + utilize.observations[0].payload_json["accounts"][9]["role"], + "use_authority_record" + ); + assert_eq!(utilize.observations[0].payload_json["accounts"][10]["role"], "burner"); + } + + #[test] + fn use_authority_family_rejects_truncated_wire_invalid_flags_and_suffixes() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::APPROVE_USE_AUTHORITY_DISCRIMINATOR], + &[(false, true); 10], + false, + "approve_use_authority_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let invalid_flags = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + utilize_wire(1).as_slice(), + &[ + (false, true), + (false, true), + (false, true), + (false, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "utilize_invalid_flags", + ), + ); + assert_eq!(invalid_flags.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::REVOKE_USE_AUTHORITY_DISCRIMINATOR, 0], + &[ + (false, true), + (true, true), + (false, false), + (false, true), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "revoke_use_authority_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn creator_signature_and_primary_sale_family_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let sign = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::SIGN_METADATA_DISCRIMINATOR], + &[(false, true), (true, false)], + false, + "sign_metadata", + ), + ); + assert_eq!(sign.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(sign.observations[0].payload_json["accounts"][0]["role"], "metadata"); + assert_eq!(sign.observations[0].payload_json["accounts"][1]["role"], "creator"); + let primary_sale = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::UPDATE_PRIMARY_SALE_HAPPENED_VIA_TOKEN_DISCRIMINATOR], + &[(false, true), (true, false), (false, false)], + true, + "update_primary_sale_happened_via_token/0", + ), + ); + assert_eq!(primary_sale.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!primary_sale.observations[0].observation_committed); + assert_eq!(primary_sale.observations[0].payload_json["accounts"][1]["role"], "owner"); + assert_eq!( + primary_sale.observations[0].payload_json["accounts"][2]["role"], + "token_account" + ); + } + + #[test] + fn creator_signature_and_primary_sale_family_reject_invalid_flags_and_suffixes() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::SIGN_METADATA_DISCRIMINATOR], + &[(false, true), (false, false)], + false, + "sign_metadata_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::UPDATE_PRIMARY_SALE_HAPPENED_VIA_TOKEN_DISCRIMINATOR, 0], + &[(false, true), (true, false), (false, false)], + false, + "primary_sale_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn sized_collection_verification_family_preserves_writable_collection_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let flags = &[ + (false, true), + (true, false), + (true, true), + (false, false), + (false, true), + (false, false), + (false, false), + ]; + for (discriminator, code) in [ + ( + crate::decoder::metadata::metaplex_token_metadata::VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR, + "verify_sized_collection_item", + ), + ( + crate::decoder::metadata::metaplex_token_metadata::UNVERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR, + "unverify_sized_collection_item", + ), + ] { + let result = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags(&[discriminator], flags, false, code), + ); + assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some(code)); + assert_eq!( + result.observations[0].payload_json["accounts"][4]["role"], + "collection_metadata" + ); + assert_eq!(result.observations[0].payload_json["accounts"][4]["writable"], true); + assert_eq!( + result.observations[0].payload_json["accounts"][6]["role"], + "collection_authority_record" + ); + } + let set_and_verify = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::SET_AND_VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR], + &[ + (false, true), + (true, false), + (true, true), + (false, false), + (false, false), + (false, true), + (false, false), + (false, false), + ], + true, + "set_and_verify_sized_collection_item", + ), + ); + assert_eq!(set_and_verify.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!set_and_verify.observations[0].observation_committed); + assert_eq!( + set_and_verify.observations[0].payload_json["accounts"][3]["role"], + "update_authority" + ); + assert_eq!(set_and_verify.observations[0].payload_json["accounts"][5]["writable"], true); + } + + #[test] + fn sized_collection_verification_rejects_non_writable_collection_and_suffix() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let invalid_collection = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR], + &[ + (false, true), + (true, false), + (true, true), + (false, false), + (false, false), + (false, false), + ], + false, + "sized-invalid-flags", + ), + ); + assert_eq!(invalid_collection.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::UNVERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR, 0], + &[ + (false, true), + (true, false), + (true, true), + (false, false), + (false, true), + (false, false), + ], + false, + "sized-suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn collection_verification_rejects_invalid_flags_and_suffixes() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let invalid_flags = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::VERIFY_COLLECTION_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (true, true), + (false, false), + (false, false), + (false, false), + ], + false, + "4", + ), + ); + assert_eq!(invalid_flags.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::UNVERIFY_COLLECTION_DISCRIMINATOR, 0], + &[(false, true), (true, true), (false, false), (false, false), (false, false)], + false, + "5", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn unknown_truncated_suffix_and_invalid_accounts_fail_closed() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let unknown = crate::InstructionDecoder::decode(&decoder, &input(&[254], 0, false, "0")); + assert_eq!(unknown.status, crate::DecoderOutcomeStatus::Unsupported); + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input(&[crate::decoder::metadata::metaplex_token_metadata::CREATE_METADATA_ACCOUNT_V3_DISCRIMINATOR], 6, false, "0"), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let mut suffix = update_wire(); + suffix.push(7); + let suffix_result = + crate::InstructionDecoder::decode(&decoder, &input(suffix.as_slice(), 2, false, "0")); + assert_eq!(suffix_result.status, crate::DecoderOutcomeStatus::Failed); + let accounts = crate::InstructionDecoder::decode( + &decoder, + &input(create_wire().as_slice(), 2, false, "0"), + ); + assert_eq!(accounts.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn audit_matrix_remains_machine_readable_and_bounded() { + let parsed = serde_json::from_str::(AUDIT_MATRIX_JSON); + assert!(parsed.is_ok()); + let matrix = match parsed { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let entries = matrix + .get("current_sdk_instruction_modules") + .and_then(serde_json::Value::as_array); + assert!(entries.is_some_and(|value| return value.len() >= MIN_SDK_INSTRUCTION_MODULES + && value.len() <= MAX_SDK_INSTRUCTION_MODULES)); + assert!( + matrix + .get("pda_families") + .and_then(serde_json::Value::as_array) + .is_some_and(|value| return value.len() >= MIN_PDA_FAMILIES) + ); + assert!( + matrix + .get("account_layouts") + .and_then(serde_json::Value::as_array) + .is_some_and(|value| return value.len() >= MIN_ACCOUNT_LAYOUTS) + ); + } + + #[test] + fn metadata_normalization_and_creator_unverification_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let puff = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::PUFF_METADATA_DISCRIMINATOR], + &[(false, true)], + false, + "puff_metadata", + ), + ); + assert_eq!(puff.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(puff.observations[0].payload_json["accounts"][0]["role"], "metadata"); + let remove = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::REMOVE_CREATOR_VERIFICATION_DISCRIMINATOR], + &[(false, true), (true, false)], + true, + "remove_creator_verification/0", + ), + ); + assert_eq!(remove.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!remove.observations[0].observation_committed); + assert_eq!(remove.observations[0].payload_json["accounts"][1]["role"], "creator"); + let token_standard = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::SET_TOKEN_STANDARD_DISCRIMINATOR], + &[(false, true), (true, false), (false, false), (false, false)], + false, + "set_token_standard", + ), + ); + assert_eq!(token_standard.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(token_standard.observations[0].payload_json["accounts"][3]["role"], "edition"); + } + + #[test] + fn metadata_normalization_family_rejects_invalid_flags_and_suffixes() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::REMOVE_CREATOR_VERIFICATION_DISCRIMINATOR], + &[(false, true), (false, false)], + false, + "remove_creator_verification_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[ + crate::decoder::metadata::metaplex_token_metadata::PUFF_METADATA_DISCRIMINATOR, + 0, + ], + &[(false, true)], + false, + "puff_metadata_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + let invalid_standard = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::SET_TOKEN_STANDARD_DISCRIMINATOR], + &[(false, true), (false, false), (false, false)], + false, + "set_token_standard_invalid", + ), + ); + assert_eq!(invalid_standard.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn collection_size_variants_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let direct = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + set_collection_size_wire(crate::decoder::metadata::metaplex_token_metadata::SET_COLLECTION_SIZE_DISCRIMINATOR, u64::MAX) + .as_slice(), + &[(false, true), (true, true), (false, false), (false, false)], + false, + "set_collection_size", + ), + ); + assert_eq!(direct.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(direct.observations[0].payload_json["parameters"]["size"], u64::MAX); + assert_eq!( + direct.observations[0].payload_json["accounts"][3]["role"], + "collection_authority_record" + ); + let bubblegum = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + set_collection_size_wire(crate::decoder::metadata::metaplex_token_metadata::BUBBLEGUM_SET_COLLECTION_SIZE_DISCRIMINATOR, 42) + .as_slice(), + &[(false, true), (true, false), (false, false), (true, false), (false, false)], + true, + "bubblegum_set_collection_size/0", + ), + ); + assert_eq!(bubblegum.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(bubblegum.observations[0].payload_json["parameters"]["size"], 42); + assert_eq!( + bubblegum.observations[0].payload_json["accounts"][3]["role"], + "bubblegum_signer" + ); + assert!(!bubblegum.observations[0].observation_committed); + } + + #[test] + fn collection_size_variants_reject_truncated_wire_invalid_flags_and_suffixes() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::SET_COLLECTION_SIZE_DISCRIMINATOR], + &[(false, true), (true, true), (false, false)], + false, + "set_collection_size_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + set_collection_size_wire(crate::decoder::metadata::metaplex_token_metadata::BUBBLEGUM_SET_COLLECTION_SIZE_DISCRIMINATOR, 1) + .as_slice(), + &[(false, true), (true, true), (false, false), (true, false)], + false, + "bubblegum_set_collection_size_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let mut suffix_wire = set_collection_size_wire( + crate::decoder::metadata::metaplex_token_metadata::SET_COLLECTION_SIZE_DISCRIMINATOR, + 2, + ); + suffix_wire.push(0); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + suffix_wire.as_slice(), + &[(false, true), (true, true), (false, false)], + false, + "set_collection_size_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn programmable_delegate_and_revoke_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let flags = &[ + (false, true), + (false, false), + (false, true), + (false, false), + (false, true), + (false, false), + (false, true), + (true, false), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let delegate = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + delegate_wire(&mpl_token_metadata::types::DelegateArgs::StandardV1 { + amount: u64::MAX, + }) + .as_slice(), + flags, + false, + "delegate", + ), + ); + assert_eq!(delegate.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + delegate.observations[0].payload_json["parameters"]["StandardV1"]["amount"], + u64::MAX + ); + assert_eq!( + delegate.observations[0].payload_json["accounts"][10]["role"], + "sysvar_instructions" + ); + let revoke = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + revoke_wire(&mpl_token_metadata::types::RevokeArgs::StandardV1).as_slice(), + flags, + true, + "revoke/0", + ), + ); + assert_eq!(revoke.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!revoke.observations[0].observation_committed); + assert_eq!(revoke.observations[0].payload_json["parameters"], "StandardV1"); + } + + #[test] + fn programmable_delegate_and_revoke_reject_truncated_invalid_and_suffix_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let flags = &[ + (false, false), + (false, false), + (false, true), + (false, false), + (false, false), + (false, false), + (false, false), + (true, false), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::DELEGATE_DISCRIMINATOR], + flags, + false, + "delegate_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let mut invalid_flags = flags.to_vec(); + invalid_flags[7] = (false, false); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + revoke_wire(&mpl_token_metadata::types::RevokeArgs::TransferV1).as_slice(), + invalid_flags.as_slice(), + false, + "revoke_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let mut suffix_wire = + delegate_wire(&mpl_token_metadata::types::DelegateArgs::CollectionV1 { + authorization_data: std::option::Option::None, + }); + suffix_wire.push(0); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags(suffix_wire.as_slice(), flags, false, "delegate_suffix"), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn programmable_lifecycle_transfer_lock_unlock_burn_and_close_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let transfer_flags = &[ + (false, true), + (false, false), + (false, true), + (false, false), + (false, false), + (false, true), + (false, false), + (false, true), + (false, true), + (true, false), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let transfer = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::TRANSFER_DISCRIMINATOR, + &mpl_token_metadata::types::TransferArgs::V1 { + amount: u64::MAX, + authorization_data: std::option::Option::None, + }, + ) + .as_slice(), + transfer_flags, + false, + "transfer", + ), + ); + assert_eq!(transfer.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(transfer.observations[0].payload_json["parameters"]["V1"]["amount"], u64::MAX); + assert_eq!( + transfer.observations[0].payload_json["accounts"][8]["role"], + "destination_token_record" + ); + let lock_flags = &[ + (true, false), + (false, false), + (false, true), + (false, false), + (false, true), + (false, false), + (false, true), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ]; + let lock = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::LOCK_DISCRIMINATOR, + &mpl_token_metadata::types::LockArgs::V1 { + authorization_data: std::option::Option::None, + }, + ) + .as_slice(), + lock_flags, + false, + "lock", + ), + ); + assert_eq!(lock.status, crate::DecoderOutcomeStatus::Decoded); + let unlock = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::UNLOCK_DISCRIMINATOR, + &mpl_token_metadata::types::UnlockArgs::V1 { + authorization_data: std::option::Option::None, + }, + ) + .as_slice(), + lock_flags, + true, + "unlock/0", + ), + ); + assert_eq!(unlock.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!unlock.observations[0].observation_committed); + let burn_flags = &[ + (true, true), + (false, true), + (false, true), + (false, true), + (false, true), + (false, true), + (false, true), + (false, false), + (false, false), + (false, true), + (false, true), + (false, false), + (false, false), + (false, false), + ]; + let burn = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::BURN_DISCRIMINATOR, + &mpl_token_metadata::types::BurnArgs::V1 { amount: u64::MAX }, + ) + .as_slice(), + burn_flags, + false, + "burn", + ), + ); + assert_eq!(burn.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(burn.observations[0].payload_json["parameters"]["V1"]["amount"], u64::MAX); + let close = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::CLOSE_ACCOUNTS_DISCRIMINATOR], + &[(false, true), (false, true), (false, true), (true, false), (false, true)], + false, + "close_accounts", + ), + ); + assert_eq!(close.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(close.observations[0].payload_json["accounts"][4]["role"], "destination"); + } + + #[test] + fn programmable_lifecycle_rejects_truncated_invalid_and_suffix_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::TRANSFER_DISCRIMINATOR], + &[(false, true); 17], + false, + "transfer_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + programmable_wire( + crate::decoder::metadata::metaplex_token_metadata::BURN_DISCRIMINATOR, + &mpl_token_metadata::types::BurnArgs::V1 { amount: 1 }, + ) + .as_slice(), + &[ + (false, true), + (false, true), + (false, true), + (false, true), + (false, true), + (false, true), + (false, true), + (false, false), + (false, false), + (false, true), + (false, true), + (false, false), + (false, false), + (false, false), + ], + false, + "burn_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[ + crate::decoder::metadata::metaplex_token_metadata::CLOSE_ACCOUNTS_DISCRIMINATOR, + 0, + ], + &[(false, true), (false, true), (false, true), (true, false), (false, true)], + false, + "close_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn legacy_delegated_freeze_and_thaw_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let flags = &[(true, true), (false, true), (false, false), (false, false), (false, false)]; + let freeze = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::FREEZE_DELEGATED_ACCOUNT_DISCRIMINATOR], + flags, + false, + "freeze_delegated_account", + ), + ); + assert_eq!(freeze.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(freeze.observations[0].payload_json["accounts"][0]["role"], "delegate"); + assert_eq!(freeze.observations[0].payload_json["accounts"][4]["role"], "token_program"); + let thaw = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::THAW_DELEGATED_ACCOUNT_DISCRIMINATOR], + flags, + true, + "thaw_delegated_account/0", + ), + ); + assert_eq!(thaw.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!thaw.observations[0].observation_committed); + } + + #[test] + fn legacy_delegated_freeze_and_thaw_reject_invalid_flags_and_suffixes() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::FREEZE_DELEGATED_ACCOUNT_DISCRIMINATOR], + &[(false, true), (false, true), (false, false), (false, false), (false, false)], + false, + "freeze_delegated_account_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::THAW_DELEGATED_ACCOUNT_DISCRIMINATOR, 0], + &[(true, true), (false, true), (false, false), (false, false), (false, false)], + false, + "thaw_delegated_account_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn legacy_nft_burn_and_printing_token_mint_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let burn_nft = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::BURN_NFT_DISCRIMINATOR], + &[ + (false, true), + (true, true), + (false, true), + (false, true), + (false, true), + (false, false), + (false, true), + ], + false, + "burn_nft", + ), + ); + assert_eq!(burn_nft.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + burn_nft.observations[0].payload_json["accounts"][6]["role"], + "collection_metadata" + ); + let burn_edition = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::BURN_EDITION_NFT_DISCRIMINATOR], + &[ + (false, true), + (true, true), + (false, true), + (false, false), + (false, true), + (false, false), + (false, true), + (false, true), + (false, true), + (false, false), + ], + true, + "burn_edition_nft/0", + ), + ); + assert_eq!(burn_edition.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!burn_edition.observations[0].observation_committed); + assert_eq!( + burn_edition.observations[0].payload_json["accounts"][8]["role"], + "edition_marker_account" + ); + let printing = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_MINT_NEW_EDITION_VIA_PRINTING_TOKEN_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (false, true), + (false, true), + (true, false), + (false, true), + (false, true), + (false, true), + (true, false), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, true), + ], + false, + "deprecated_printing_token", + ), + ); + assert_eq!(printing.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + printing.observations[0].payload_json["accounts"][15]["role"], + "reservation_list" + ); + } + + #[test] + fn legacy_nft_burn_and_printing_token_mint_reject_invalid_flags_and_suffixes() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::BURN_NFT_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (false, true), + (false, true), + (false, true), + (false, false), + ], + false, + "burn_nft_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::BURN_EDITION_NFT_DISCRIMINATOR, 0], + &[ + (false, true), + (true, true), + (false, true), + (false, false), + (false, true), + (false, false), + (false, true), + (false, true), + (false, true), + (false, false), + ], + false, + "burn_edition_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + let truncated_accounts = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_MINT_NEW_EDITION_VIA_PRINTING_TOKEN_DISCRIMINATOR], + &[(false, true); 14], + false, + "deprecated_printing_token_truncated", + ), + ); + assert_eq!(truncated_accounts.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn escrow_and_collect_family_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let create = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::CREATE_ESCROW_ACCOUNT_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (false, false), + (false, false), + (false, false), + (true, true), + (false, false), + (false, false), + (true, false), + ], + false, + "create_escrow_account", + ), + ); + assert_eq!(create.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(create.observations[0].payload_json["accounts"][8]["role"], "authority"); + let close = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::CLOSE_ESCROW_ACCOUNT_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (false, false), + (false, false), + (false, false), + (true, true), + (false, false), + (false, false), + ], + true, + "close_escrow_account/0", + ), + ); + assert_eq!(close.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!close.observations[0].observation_committed); + let mut transfer_wire = vec![ + crate::decoder::metadata::metaplex_token_metadata::TRANSFER_OUT_OF_ESCROW_DISCRIMINATOR, + ]; + transfer_wire.extend_from_slice(&u64::MAX.to_le_bytes()); + let transfer = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + transfer_wire.as_slice(), + &[ + (false, false), + (false, true), + (true, true), + (false, false), + (false, true), + (false, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (true, false), + ], + false, + "transfer_out_of_escrow", + ), + ); + assert_eq!(transfer.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(transfer.observations[0].payload_json["parameters"]["amount"], u64::MAX); + assert_eq!(transfer.observations[0].payload_json["accounts"][12]["role"], "authority"); + let collect = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::COLLECT_DISCRIMINATOR], + &[(true, false), (false, false)], + false, + "collect", + ), + ); + assert_eq!(collect.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(collect.observations[0].payload_json["accounts"][1]["role"], "recipient"); + } + + #[test] + fn escrow_and_collect_family_reject_truncated_invalid_and_suffix_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let truncated = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::TRANSFER_OUT_OF_ESCROW_DISCRIMINATOR], + &[ + (false, false), + (false, true), + (true, true), + (false, false), + (false, true), + (false, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "transfer_out_of_escrow_truncated", + ), + ); + assert_eq!(truncated.status, crate::DecoderOutcomeStatus::Failed); + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::CREATE_ESCROW_ACCOUNT_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (false, false), + (false, false), + (false, false), + (false, true), + (false, false), + (false, false), + ], + false, + "create_escrow_account_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::COLLECT_DISCRIMINATOR, 0], + &[(true, false), (false, false)], + false, + "collect_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn resize_and_migrate_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let resize = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::RESIZE_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (false, false), + (true, true), + (true, false), + (false, false), + (false, false), + ], + false, + "resize", + ), + ); + assert_eq!(resize.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!(resize.observations[0].payload_json["accounts"][4]["role"], "authority"); + let migrate = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::MIGRATE_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (false, true), + (false, false), + (false, false), + (true, true), + (true, false), + (false, false), + (false, false), + (false, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + true, + "migrate/0", + ), + ); + assert_eq!(migrate.status, crate::DecoderOutcomeStatus::Decoded); + assert!(!migrate.observations[0].observation_committed); + assert_eq!(migrate.observations[0].payload_json["accounts"][9]["role"], "token_record"); + } + + #[test] + fn resize_and_migrate_reject_invalid_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let invalid_resize = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::RESIZE_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (false, false), + (false, true), + (true, false), + (false, false), + (false, false), + ], + false, + "resize_invalid", + ), + ); + assert_eq!(invalid_resize.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::MIGRATE_DISCRIMINATOR, 0], + &[ + (false, true), + (false, true), + (false, true), + (false, false), + (false, false), + (true, true), + (true, false), + (false, false), + (false, false), + (false, true), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "migrate_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } + + #[test] + fn omitted_historical_discriminators_decode_exact_contracts() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + type HistoricalCase<'a> = (&'a [u8], &'a [(bool, bool)], &'a str, usize, &'a str); + let cases: &[HistoricalCase<'_>] = &[ + ( + &[crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_CREATE_MASTER_EDITION_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (false, true), + (false, true), + (true, false), + (true, false), + (true, false), + (false, false), + (true, false), + (false, false), + (false, false), + (false, false), + (true, false), + ], + "deprecated_create_master_edition", + 12, + "one_time_printing_authorization_mint_authority", + ), + ( + &[crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_SET_RESERVATION_LIST_DISCRIMINATOR], + &[(false, true), (false, true), (true, false)], + "deprecated_set_reservation_list", + 2, + "resource", + ), + ( + &[crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_CREATE_RESERVATION_LIST_DISCRIMINATOR], + &[ + (false, true), + (true, false), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + "deprecated_create_reservation_list", + 4, + "resource", + ), + ( + &[crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_MINT_PRINTING_TOKENS_VIA_TOKEN_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (false, true), + (false, true), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + "deprecated_mint_printing_tokens_via_token", + 4, + "burn_authority", + ), + ( + &[crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_MINT_PRINTING_TOKENS_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (true, false), + (false, false), + (false, false), + (false, false), + (false, false), + ], + "deprecated_mint_printing_tokens", + 2, + "update_authority", + ), + ( + &[crate::decoder::metadata::metaplex_token_metadata::CREATE_MASTER_EDITION_DISCRIMINATOR], + &[ + (false, true), + (false, true), + (true, false), + (true, false), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + ], + "create_master_edition", + 4, + "payer", + ), + ]; + for (wire, flags, path, role_index, role) in cases { + let decoded = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags(wire, flags, false, path), + ); + assert_eq!(decoded.status, crate::DecoderOutcomeStatus::Decoded); + assert_eq!( + decoded.observations[0].payload_json["accounts"][*role_index]["role"], + *role + ); + } + } + + #[test] + fn omitted_historical_discriminators_reject_invalid_wires() { + let decoder = + crate::decoder::metadata::metaplex_token_metadata::MetadataMetaplexTokenMetadataDecoder; + let invalid = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_SET_RESERVATION_LIST_DISCRIMINATOR], + &[(false, true), (false, true), (false, false)], + false, + "deprecated_set_reservation_list_invalid", + ), + ); + assert_eq!(invalid.status, crate::DecoderOutcomeStatus::Failed); + let suffix = crate::InstructionDecoder::decode( + &decoder, + &input_with_flags( + &[crate::decoder::metadata::metaplex_token_metadata::CREATE_MASTER_EDITION_DISCRIMINATOR, 0], + &[ + (false, true), + (false, true), + (true, false), + (true, false), + (true, true), + (false, false), + (false, false), + (false, false), + (false, false), + ], + false, + "create_master_edition_suffix", + ), + ); + assert_eq!(suffix.status, crate::DecoderOutcomeStatus::Failed); + } +} diff --git a/kb-lib/src/decoder/metadata/metaplex_token_metadata/instruction.rs b/kb-lib/src/decoder/metadata/metaplex_token_metadata/instruction.rs new file mode 100644 index 0000000..0815ff1 --- /dev/null +++ b/kb-lib/src/decoder/metadata/metaplex_token_metadata/instruction.rs @@ -0,0 +1,2746 @@ +// file: kb-lib/src/decoder/metadata/metaplex_token_metadata/instruction.rs +// version: 26 + +//! Bounded Metaplex Token Metadata instruction wire decoding. + +use base64::Engine; // rust-rules: trait-import +use sha2::Digest; // rust-rules: trait-import + +pub(crate) struct ParsedInstruction { + pub(crate) code: &'static str, + pub(crate) discriminator: u8, + pub(crate) parameters: serde_json::Value, + pub(crate) consumed: usize, +} + +#[derive(Clone, Debug)] +struct ResolvedAccount { + position: usize, + account_index: u64, + account_key: std::string::String, + signer: bool, + writable: bool, + source: serde_json::Value, +} + +pub(crate) fn entry_for_discriminator( + discriminator: u8, +) -> std::option::Option<(u8, &'static str, bool)> { + return crate::decoder::metadata::metaplex_token_metadata::INSTRUCTION_ENTRIES + .iter() + .copied() + .find(|(candidate, _, _)| return *candidate == discriminator); +} + +pub(crate) fn payload_discriminator( + input: &crate::CoreInstructionReplayInput, +) -> std::option::Option { + return decode_payload(input).ok().and_then(|value| return value.first().copied()); +} + +pub(crate) fn decode(input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult { + let bytes = match decode_payload(input) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return failed( + std::option::Option::None, + "metaplex_token_metadata_payload_unavailable", + error.to_string(), + ); + }, + }; + let discriminator = match bytes.first().copied() { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return failed( + std::option::Option::None, + "metaplex_token_metadata_payload_empty", + "Metaplex Token Metadata instruction payload is empty".to_string(), + ); + }, + }; + let entry = match crate::decoder::metadata::metaplex_token_metadata::entry_for_discriminator( + discriminator, + ) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return crate::DecoderExecutionResult { + status: crate::DecoderOutcomeStatus::Unsupported, + recognized_entry_code: std::option::Option::None, + observations: std::vec::Vec::new(), + diagnostics: std::vec![crate::DecoderDiagnostic { + code: "unknown_metaplex_token_metadata_discriminator".to_string(), + message: format!( + "unknown Metaplex Token Metadata discriminator {discriminator}; payloadPrefixHex={}", + hex_prefix(bytes.as_slice()) + ), + retriable: false, + }], + }; + }, + }; + let parsed = match parse_instruction(bytes.as_slice()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(message) => { + return failed( + std::option::Option::Some(entry.1), + "malformed_metaplex_token_metadata_instruction", + format!("{message}; payloadPrefixHex={}", hex_prefix(bytes.as_slice())), + ); + }, + }; + let accounts = match resolve_accounts(input) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return failed( + std::option::Option::Some(parsed.code), + "metaplex_token_metadata_accounts_malformed", + error.to_string(), + ); + }, + }; + let validation = validate_accounts(parsed.code, accounts.as_slice()); + if let std::result::Result::Err(message) = validation { + return failed( + std::option::Option::Some(parsed.code), + "metaplex_token_metadata_accounts_invalid", + message, + ); + } + let suffix_length = bytes.len().saturating_sub(parsed.consumed); + if suffix_length != 0 { + return failed( + std::option::Option::Some(parsed.code), + "metaplex_token_metadata_suffix_forbidden", + format!("{} unexpected suffix bytes", suffix_length), + ); + } + let observation = observation( + input, + &parsed, + project_accounts(parsed.code, accounts.as_slice()), + bytes.as_slice(), + ); + return crate::DecoderExecutionResult { + status: crate::DecoderOutcomeStatus::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::CoreInstructionReplayInput) -> kb_core::Result> { + let payload = match input.instruction_payload_json.as_ref() { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::invalid_state( + "Metaplex Token Metadata instruction payload is not retained", + )); + }, + }; + let encoded = match payload.get("dataBase64").and_then(serde_json::Value::as_str) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::invalid_state( + "Metaplex Token Metadata payload does not contain dataBase64", + )); + }, + }; + if encoded.len() > crate::decoder::metadata::metaplex_token_metadata::MAX_BASE64_BYTES { + return std::result::Result::Err(kb_core::Error::invalid_state(format!( + "Metaplex Token Metadata base64 payload exceeds {} bytes", + crate::decoder::metadata::metaplex_token_metadata::MAX_BASE64_BYTES + ))); + } + let decoded = match base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(kb_core::Error::invalid_state(format!( + "Metaplex Token Metadata payload is not valid base64: {error}" + ))); + }, + }; + if decoded.len() > crate::decoder::metadata::metaplex_token_metadata::MAX_INSTRUCTION_BYTES { + return std::result::Result::Err(kb_core::Error::invalid_state(format!( + "Metaplex Token Metadata payload exceeds {} decoded bytes", + crate::decoder::metadata::metaplex_token_metadata::MAX_INSTRUCTION_BYTES + ))); + } + return std::result::Result::Ok(decoded); +} + +fn parse_instruction( + bytes: &[u8], +) -> std::result::Result< + crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction, + std::string::String, +> { + let discriminator = match bytes.first().copied() { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err("empty instruction".to_string()); + }, + }; + return match discriminator { + crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_CREATE_MASTER_EDITION_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "deprecated_create_master_edition", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_SET_RESERVATION_LIST_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "deprecated_set_reservation_list", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_CREATE_RESERVATION_LIST_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "deprecated_create_reservation_list", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_MINT_PRINTING_TOKENS_VIA_TOKEN_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "deprecated_mint_printing_tokens_via_token", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_MINT_PRINTING_TOKENS_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "deprecated_mint_printing_tokens", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::CREATE_MASTER_EDITION_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "create_master_edition", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::DEPRECATED_MINT_NEW_EDITION_VIA_PRINTING_TOKEN_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "deprecated_mint_new_edition_from_master_edition_via_printing_token", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_TOKEN_DISCRIMINATOR + | crate::decoder::metadata::metaplex_token_metadata::MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_VAULT_PROXY_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("MintNewEditionFromMasterEdition edition Borsh decode failed: {error}")), + }; + let code = if discriminator + == crate::decoder::metadata::metaplex_token_metadata::MINT_NEW_EDITION_FROM_MASTER_EDITION_VIA_TOKEN_DISCRIMINATOR + { + "mint_new_edition_from_master_edition_via_token" + } else { + "mint_new_edition_from_master_edition_via_vault_proxy" + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code, + discriminator, + parameters: serde_json::json!({"edition": args.edition}), + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::CONVERT_MASTER_EDITION_V1_TO_V2_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "convert_master_edition_v1_to_v2", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::CREATE_MASTER_EDITION_V3_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let max_supply = + match as borsh_0_10::BorshDeserialize>::deserialize( + &mut remaining, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "CreateMasterEditionV3 max_supply Borsh decode failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "create_master_edition_v3", + discriminator, + parameters: serde_json::json!({"max_supply": max_supply}), + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::UPDATE_PRIMARY_SALE_HAPPENED_VIA_TOKEN_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "update_primary_sale_happened_via_token", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::SIGN_METADATA_DISCRIMINATOR => std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "sign_metadata", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }), + crate::decoder::metadata::metaplex_token_metadata::PUFF_METADATA_DISCRIMINATOR => std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "puff_metadata", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }), + crate::decoder::metadata::metaplex_token_metadata::REMOVE_CREATOR_VERIFICATION_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "remove_creator_verification", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::SET_TOKEN_STANDARD_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "set_token_standard", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::CREATE_METADATA_ACCOUNT_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let data = + match ::deserialize( + &mut remaining, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "CreateMetadataAccount data Borsh decode failed: {error}" + )); + }, + }; + let is_mutable = + match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "CreateMetadataAccount is_mutable Borsh decode failed: {error}" + )); + }, + }; + match validate_data(&data) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + let data_json = match serde_json::to_value(&data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "CreateMetadataAccount data JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "create_metadata_account", + discriminator, + parameters: serde_json::json!({"data": data_json, "is_mutable": is_mutable}), + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::UPDATE_METADATA_ACCOUNT_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let data = match as borsh_0_10::BorshDeserialize>::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("UpdateMetadataAccount data Borsh decode failed: {error}")), + }; + let new_update_authority = + match as borsh_0_10::BorshDeserialize>::deserialize( + &mut remaining, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "UpdateMetadataAccount authority Borsh decode failed: {error}" + )); + }, + }; + let primary_sale_happened = + match as borsh_0_10::BorshDeserialize>::deserialize( + &mut remaining, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "UpdateMetadataAccount primary sale Borsh decode failed: {error}" + )); + }, + }; + if let std::option::Option::Some(value) = data.as_ref() { + match validate_data(value) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } + let data_json = match serde_json::to_value(&data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "UpdateMetadataAccount data JSON projection failed: {error}" + )); + }, + }; + let authority_json = new_update_authority.map(|value| return hex_bytes(&value)); + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "update_metadata_account", + discriminator, + parameters: serde_json::json!({"data": data_json, "new_update_authority_hex": authority_json, "primary_sale_happened": primary_sale_happened}), + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::CREATE_METADATA_ACCOUNT_V2_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let data = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("CreateMetadataAccountV2 data Borsh decode failed: {error}")), + }; + let is_mutable = + match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "CreateMetadataAccountV2 is_mutable Borsh decode failed: {error}" + )); + }, + }; + match validate_data_v2(&data) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + let data_json = match serde_json::to_value(&data) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "CreateMetadataAccountV2 data JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "create_metadata_account_v2", + discriminator, + parameters: serde_json::json!({ + "data": data_json, + "is_mutable": is_mutable, + }), + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::CREATE_METADATA_ACCOUNT_V3_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("CreateMetadataAccountV3 Borsh decode failed: {error}")), + }; + match validate_data_v2(&args.data) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "CreateMetadataAccountV3 JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "create_metadata_account_v3", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::TRANSFER_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("TransferArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "TransferArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "transfer", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::LOCK_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("LockArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "LockArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "lock", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::UNLOCK_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("UnlockArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "UnlockArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "unlock", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::CREATE_ESCROW_ACCOUNT_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "create_escrow_account", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::CLOSE_ESCROW_ACCOUNT_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "close_escrow_account", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::TRANSFER_OUT_OF_ESCROW_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let amount = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "TransferOutOfEscrow amount Borsh decode failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "transfer_out_of_escrow", + discriminator, + parameters: serde_json::json!({"amount": amount}), + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::COLLECT_DISCRIMINATOR => std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "collect", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }), + crate::decoder::metadata::metaplex_token_metadata::BURN_NFT_DISCRIMINATOR => std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "burn_nft", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }), + crate::decoder::metadata::metaplex_token_metadata::BURN_EDITION_NFT_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "burn_edition_nft", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::CREATE_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("CreateArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "CreateArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "create", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::MINT_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("MintArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "MintArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "mint", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::PRINT_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("PrintArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "PrintArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "print", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::MIGRATE_DISCRIMINATOR => std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "migrate", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }), + crate::decoder::metadata::metaplex_token_metadata::RESIZE_DISCRIMINATOR => std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "resize", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }), + crate::decoder::metadata::metaplex_token_metadata::USE_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("UseArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "UseArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "use", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::VERIFY_DISCRIMINATOR | crate::decoder::metadata::metaplex_token_metadata::UNVERIFY_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("VerificationArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "VerificationArgs JSON projection failed: {error}" + )); + }, + }; + let code = + if discriminator == crate::decoder::metadata::metaplex_token_metadata::VERIFY_DISCRIMINATOR { "verify" } else { "unverify" }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code, + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::UPDATE_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("UpdateArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "UpdateArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "update", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::BURN_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("BurnArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "BurnArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "burn", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::CLOSE_ACCOUNTS_DISCRIMINATOR => std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "close_accounts", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }), + crate::decoder::metadata::metaplex_token_metadata::DELEGATE_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("DelegateArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "DelegateArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "delegate", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::REVOKE_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("RevokeArgs Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "RevokeArgs JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "revoke", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::SET_COLLECTION_SIZE_DISCRIMINATOR + | crate::decoder::metadata::metaplex_token_metadata::BUBBLEGUM_SET_COLLECTION_SIZE_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("SetCollectionSize Borsh decode failed: {error}")), + }; + let code = if discriminator == crate::decoder::metadata::metaplex_token_metadata::SET_COLLECTION_SIZE_DISCRIMINATOR { + "set_collection_size" + } else { + "bubblegum_set_collection_size" + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code, + discriminator, + parameters: serde_json::json!({"size": args.size}), + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::VERIFY_COLLECTION_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "verify_collection", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::UNVERIFY_COLLECTION_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "unverify_collection", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::SET_AND_VERIFY_COLLECTION_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "set_and_verify_collection", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::APPROVE_COLLECTION_AUTHORITY_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "approve_collection_authority", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::REVOKE_COLLECTION_AUTHORITY_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "revoke_collection_authority", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "verify_sized_collection_item", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::UNVERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "unverify_sized_collection_item", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::SET_AND_VERIFY_SIZED_COLLECTION_ITEM_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "set_and_verify_sized_collection_item", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::UTILIZE_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("Utilize Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "Utilize JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "utilize", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::APPROVE_USE_AUTHORITY_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("ApproveUseAuthority Borsh decode failed: {error}")), + }; + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "ApproveUseAuthority JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "approve_use_authority", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + crate::decoder::metadata::metaplex_token_metadata::REVOKE_USE_AUTHORITY_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "revoke_use_authority", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::FREEZE_DELEGATED_ACCOUNT_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "freeze_delegated_account", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::THAW_DELEGATED_ACCOUNT_DISCRIMINATOR => { + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "thaw_delegated_account", + discriminator, + parameters: serde_json::json!({}), + consumed: 1, + }) + }, + crate::decoder::metadata::metaplex_token_metadata::UPDATE_METADATA_ACCOUNT_V2_DISCRIMINATOR => { + let mut remaining = &bytes[1..]; + let args = match ::deserialize(&mut remaining) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(format!("UpdateMetadataAccountV2 Borsh decode failed: {error}")), + }; + if let std::option::Option::Some(data) = args.data.as_ref() { + match validate_data_v2(data) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(error) => return std::result::Result::Err(error), + } + } + let parameters = match serde_json::to_value(&args) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => { + return std::result::Result::Err(format!( + "UpdateMetadataAccountV2 JSON projection failed: {error}" + )); + }, + }; + std::result::Result::Ok(crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction { + code: "update_metadata_account_v2", + discriminator, + parameters, + consumed: bytes.len().saturating_sub(remaining.len()), + }) + }, + _ => std::result::Result::Err(format!("unsupported discriminator {discriminator}")), + }; +} + +fn validate_data( + data: &mpl_token_metadata::types::Data, +) -> std::result::Result<(), std::string::String> { + if data.name.len() > 32 { + return std::result::Result::Err("metadata name exceeds 32 bytes".to_string()); + } + if data.symbol.len() > 10 { + return std::result::Result::Err("metadata symbol exceeds 10 bytes".to_string()); + } + if data.uri.len() > 200 { + return std::result::Result::Err("metadata URI exceeds 200 bytes".to_string()); + } + if data.seller_fee_basis_points > 10_000 { + return std::result::Result::Err("metadata seller fee exceeds 10000".to_string()); + } + if data.creators.as_ref().is_some_and(|creators| return creators.len() > 5) { + return std::result::Result::Err("metadata creators exceed 5 entries".to_string()); + } + return std::result::Result::Ok(()); +} + +fn hex_bytes(bytes: &[u8]) -> std::string::String { + return bytes + .iter() + .map(|value| return format!("{value:02x}")) + .collect::>() + .join(""); +} + +fn validate_data_v2( + data: &mpl_token_metadata::types::DataV2, +) -> std::result::Result<(), std::string::String> { + if data.name.len() > 32 { + return std::result::Result::Err("metadata name exceeds 32 bytes".to_string()); + } + if data.symbol.len() > 10 { + return std::result::Result::Err("metadata symbol exceeds 10 bytes".to_string()); + } + if data.uri.len() > 200 { + return std::result::Result::Err("metadata URI exceeds 200 bytes".to_string()); + } + if data.seller_fee_basis_points > 10_000 { + return std::result::Result::Err("seller fee basis points exceed 10000".to_string()); + } + if data.creators.as_ref().is_some_and(|creators| return creators.len() > 5) { + return std::result::Result::Err("metadata creators exceed 5 entries".to_string()); + } + return std::result::Result::Ok(()); +} + +fn resolve_accounts( + input: &crate::CoreInstructionReplayInput, +) -> kb_core::Result> { + let instruction_accounts = match input.instruction_accounts_json.as_array() { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::invalid_state( + "Metaplex Token Metadata instruction accounts must be a JSON array", + )); + }, + }; + if instruction_accounts.len() + > crate::decoder::metadata::metaplex_token_metadata::MAX_INSTRUCTION_ACCOUNTS + { + return std::result::Result::Err(kb_core::Error::invalid_state( + "Metaplex Token Metadata instruction account count exceeds bound", + )); + } + let account_keys = match input.account_keys_json.as_array() { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::invalid_state( + "Metaplex Token Metadata transaction account keys must be a JSON array", + )); + }, + }; + let mut output = std::vec::Vec::with_capacity(instruction_accounts.len()); + for (position, account) in instruction_accounts.iter().enumerate() { + let account_index = match account.get("accountIndex").and_then(serde_json::Value::as_u64) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::invalid_state(format!( + "Metaplex Token Metadata instruction account {position} has no accountIndex" + ))); + }, + }; + let account_key = match account.get("accountKey").and_then(serde_json::Value::as_str) { + std::option::Option::Some(value) if !value.trim().is_empty() => value, + _ => { + return std::result::Result::Err(kb_core::Error::invalid_state(format!( + "Metaplex Token Metadata 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(kb_core::Error::invalid_state(format!( + "Metaplex Token Metadata account index {account_index} is not resolved" + ))); + }, + }; + if resolved.get("accountKey").and_then(serde_json::Value::as_str) + != std::option::Option::Some(account_key) + { + return std::result::Result::Err(kb_core::Error::invalid_state(format!( + "Metaplex Token Metadata account index {account_index} resolves to a different key" + ))); + } + let signer = match resolved.get("signer").and_then(serde_json::Value::as_bool) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::invalid_state(format!( + "Metaplex Token Metadata account index {account_index} has no signer flag" + ))); + }, + }; + let writable = match resolved.get("writable").and_then(serde_json::Value::as_bool) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(kb_core::Error::invalid_state(format!( + "Metaplex Token Metadata account index {account_index} has no writable flag" + ))); + }, + }; + output.push(ResolvedAccount { + position, + account_index, + account_key: account_key.to_string(), + signer, + writable, + source: resolved.get("source").cloned().unwrap_or(serde_json::Value::Null), + }); + } + return std::result::Result::Ok(output); +} + +fn validate_accounts( + code: &str, + accounts: &[ResolvedAccount], +) -> std::result::Result<(), std::string::String> { + if code == "create_escrow_account" { + if accounts.len() < 8 { + return std::result::Result::Err(format!( + "create_escrow_account requires at least 8 accounts, received {}", + accounts.len() + )); + } + if let std::option::Option::Some(authority) = accounts.get(8) { + if !authority.signer || authority.writable { + return std::result::Result::Err( + "create_escrow_account optional authority must be readonly signer".to_string(), + ); + } + } + } + if code == "transfer_out_of_escrow" { + if accounts.len() < 12 { + return std::result::Result::Err(format!( + "transfer_out_of_escrow requires at least 12 accounts, received {}", + accounts.len() + )); + } + if let std::option::Option::Some(authority) = accounts.get(12) { + if !authority.signer || authority.writable { + return std::result::Result::Err( + "transfer_out_of_escrow optional authority must be readonly signer".to_string(), + ); + } + } + } + if code == "create" { + if accounts.len() < 9 { + return std::result::Result::Err(format!( + "create requires 9 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (0, false, true), + (2, true, true), + (3, true, false), + (4, true, true), + (5, true, false), + (6, false, false), + (7, false, false), + (8, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("create is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "create account {index} has invalid signer/writable flags" + )); + } + } + let master_edition = match accounts.get(1) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err( + "create is missing master edition placeholder".to_string(), + ); + }, + }; + if master_edition.signer { + return std::result::Result::Err( + "create master edition account must not sign".to_string(), + ); + } + return std::result::Result::Ok(()); + } + if code == "mint" { + if accounts.len() < 15 { + return std::result::Result::Err(format!( + "mint requires 15 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (0, false, true), + (2, false, false), + (5, false, true), + (6, true, false), + (8, true, true), + (9, false, false), + (10, false, false), + (11, false, false), + (12, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("mint is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "mint account {index} has invalid signer/writable flags" + )); + } + } + for index in [1_usize, 3_usize, 4_usize, 7_usize, 13_usize, 14_usize] { + let account = match accounts.get(index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "mint is missing optional placeholder account {index}" + )); + }, + }; + if account.signer { + return std::result::Result::Err(format!( + "mint optional account {index} must not sign" + )); + } + } + return std::result::Result::Ok(()); + } + if code == "print" { + if accounts.len() < 18 { + return std::result::Result::Err(format!( + "print requires 18 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (0, false, true), + (1, false, true), + (2, true, true), + (3, false, false), + (4, false, true), + (5, true, false), + (7, false, true), + (8, false, true), + (9, true, true), + (10, true, false), + (11, false, false), + (12, false, false), + (13, false, false), + (14, false, false), + (15, false, false), + (16, false, false), + (17, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("print is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "print account {index} has invalid signer/writable flags" + )); + } + } + let edition_token_record = match accounts.get(6) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err( + "print is missing edition token record placeholder".to_string(), + ); + }, + }; + if edition_token_record.signer { + return std::result::Result::Err( + "print edition token record must not sign".to_string(), + ); + } + return std::result::Result::Ok(()); + } + if code == "update" { + if accounts.len() < 11 { + return std::result::Result::Err(format!( + "update requires 11 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (0, true, false), + (3, false, false), + (4, false, true), + (6, true, true), + (7, false, false), + (8, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("update is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "update account {index} has invalid signer/writable flags" + )); + } + } + for index in [1_usize, 2_usize, 5_usize, 9_usize, 10_usize] { + let account = match accounts.get(index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "update is missing optional placeholder account {index}" + )); + }, + }; + if account.signer || account.writable { + return std::result::Result::Err(format!( + "update optional account {index} must be readonly and non-signer" + )); + } + } + return std::result::Result::Ok(()); + } + if code == "resize" { + if accounts.len() < 7 { + return std::result::Result::Err(format!( + "resize requires 7 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (0, false, true), + (1, false, true), + (2, false, false), + (3, true, true), + (6, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("resize is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "resize account {index} has invalid signer/writable flags" + )); + } + } + for index in [4_usize, 5_usize] { + let account = match accounts.get(index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "resize is missing optional placeholder account {index}" + )); + }, + }; + if account.writable { + return std::result::Result::Err(format!( + "resize optional account {index} must be readonly" + )); + } + if index == 5 && account.signer { + return std::result::Result::Err("resize optional token must not sign".to_string()); + } + } + return std::result::Result::Ok(()); + } + if code == "migrate" { + if accounts.len() < 15 { + return std::result::Result::Err(format!( + "migrate requires 15 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (0, false, true), + (1, false, true), + (2, false, true), + (3, false, false), + (4, false, false), + (5, true, true), + (6, true, false), + (7, false, false), + (8, false, false), + (9, false, true), + (10, false, false), + (11, false, false), + (12, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("migrate is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "migrate account {index} has invalid signer/writable flags" + )); + } + } + for index in [13_usize, 14_usize] { + let account = match accounts.get(index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "migrate is missing optional placeholder account {index}" + )); + }, + }; + if account.signer || account.writable { + return std::result::Result::Err(format!( + "migrate optional account {index} must be readonly and non-signer" + )); + } + } + return std::result::Result::Ok(()); + } + if code == "transfer" { + if accounts.len() < 17 { + return std::result::Result::Err(format!( + "transfer requires 17 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (0, false, true), + (1, false, false), + (2, false, true), + (3, false, false), + (4, false, false), + (5, false, true), + (9, true, false), + (10, true, true), + (11, false, false), + (12, false, false), + (13, false, false), + (14, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "transfer is missing account {index}" + )); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "transfer account {index} has invalid signer/writable flags" + )); + } + } + for index in [6_usize, 7_usize, 8_usize, 15_usize, 16_usize] { + let account = match accounts.get(index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "transfer is missing optional placeholder account {index}" + )); + }, + }; + if account.signer { + return std::result::Result::Err(format!( + "transfer optional account {index} must not sign" + )); + } + } + return std::result::Result::Ok(()); + } + if code == "lock" || code == "unlock" { + if accounts.len() < 13 { + return std::result::Result::Err(format!( + "{code} requires 13 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (0, true, false), + (2, false, true), + (3, false, false), + (4, false, true), + (7, true, true), + (8, false, false), + (9, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("{code} is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "{code} account {index} has invalid signer/writable flags" + )); + } + } + for index in [1_usize, 5_usize, 6_usize, 10_usize, 11_usize, 12_usize] { + let account = match accounts.get(index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "{code} is missing optional placeholder account {index}" + )); + }, + }; + if account.signer { + return std::result::Result::Err(format!( + "{code} optional account {index} must not sign" + )); + } + } + return std::result::Result::Ok(()); + } + if code == "use" { + if accounts.len() < 12 { + return std::result::Result::Err(format!( + "use requires 12 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (0, true, false), + (3, false, false), + (4, false, true), + (6, true, false), + (7, false, false), + (8, false, false), + (9, false, false), + (10, false, false), + (11, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("use is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "use account {index} has invalid signer/writable flags" + )); + } + } + for index in [1_usize, 2_usize, 5_usize] { + let account = match accounts.get(index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "use is missing optional placeholder account {index}" + )); + }, + }; + if account.signer { + return std::result::Result::Err(format!( + "use optional account {index} must not sign" + )); + } + } + return std::result::Result::Ok(()); + } + if code == "verify" || code == "unverify" { + if accounts.len() < 8 { + return std::result::Result::Err(format!( + "{code} requires 8 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = + &[(0, true, false), (2, false, true), (6, false, false), (7, false, false)]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("{code} is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "{code} account {index} has invalid signer/writable flags" + )); + } + } + for index in [1_usize, 3_usize, 4_usize, 5_usize] { + let account = match accounts.get(index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "{code} is missing optional placeholder account {index}" + )); + }, + }; + if account.signer { + return std::result::Result::Err(format!( + "{code} optional account {index} must not sign" + )); + } + } + return std::result::Result::Ok(()); + } + if code == "burn" { + if accounts.len() < 14 { + return std::result::Result::Err(format!( + "burn requires 14 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (0, true, true), + (2, false, true), + (4, false, true), + (5, false, true), + (11, false, false), + (12, false, false), + (13, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("burn is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "burn account {index} has invalid signer/writable flags" + )); + } + } + for index in [1_usize, 3_usize, 6_usize, 7_usize, 8_usize, 9_usize, 10_usize] { + let account = match accounts.get(index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "burn is missing optional placeholder account {index}" + )); + }, + }; + if account.signer { + return std::result::Result::Err(format!( + "burn optional account {index} must not sign" + )); + } + } + return std::result::Result::Ok(()); + } + if code == "delegate" || code == "revoke" { + if accounts.len() < 14 { + return std::result::Result::Err(format!( + "{code} requires 14 positional accounts, received {}", + accounts.len() + )); + } + let mandatory: &[(usize, bool, bool)] = &[ + (1, false, false), + (2, false, true), + (3, false, false), + (5, false, false), + (7, true, false), + (8, true, true), + (9, false, false), + (10, false, false), + (11, false, false), + (12, false, false), + (13, false, false), + ]; + for (index, signer, writable) in mandatory { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("{code} is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "{code} account {index} has invalid signer/writable flags" + )); + } + } + for index in [0_usize, 4_usize, 6_usize] { + let account = match accounts.get(index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!( + "{code} is missing optional placeholder account {index}" + )); + }, + }; + if account.signer { + return std::result::Result::Err(format!( + "{code} optional account {index} must not sign" + )); + } + } + return std::result::Result::Ok(()); + } + let (required, exact_flags): (usize, &[(usize, bool, bool)]) = match code { + "close_accounts" => ( + 5, + &[ + (0, false, true), + (1, false, true), + (2, false, true), + (3, true, false), + (4, false, true), + ], + ), + "create_escrow_account" => ( + 8, + &[ + (0, false, true), + (1, false, true), + (2, false, false), + (3, false, false), + (4, false, false), + (5, true, true), + (6, false, false), + (7, false, false), + ], + ), + "close_escrow_account" => ( + 8, + &[ + (0, false, true), + (1, false, true), + (2, false, false), + (3, false, false), + (4, false, false), + (5, true, true), + (6, false, false), + (7, false, false), + ], + ), + "transfer_out_of_escrow" => ( + 12, + &[ + (0, false, false), + (1, false, true), + (2, true, true), + (3, false, false), + (4, false, true), + (5, false, true), + (6, false, false), + (7, false, false), + (8, false, false), + (9, false, false), + (10, false, false), + (11, false, false), + ], + ), + "collect" => (2, &[(0, true, false), (1, false, false)]), + "deprecated_create_master_edition" => ( + 13, + &[ + (0, false, true), + (1, false, true), + (2, false, true), + (3, false, true), + (4, true, false), + (5, true, false), + (6, true, false), + (7, false, false), + (8, true, false), + (9, false, false), + (10, false, false), + (11, false, false), + (12, true, false), + ], + ), + "deprecated_set_reservation_list" => { + (3, &[(0, false, true), (1, false, true), (2, true, false)]) + }, + "deprecated_create_reservation_list" => ( + 8, + &[ + (0, false, true), + (1, true, false), + (2, true, false), + (3, false, false), + (4, false, false), + (5, false, false), + (6, false, false), + (7, false, false), + ], + ), + "deprecated_mint_printing_tokens_via_token" => ( + 9, + &[ + (0, false, true), + (1, false, true), + (2, false, true), + (3, false, true), + (4, true, false), + (5, false, false), + (6, false, false), + (7, false, false), + (8, false, false), + ], + ), + "deprecated_mint_printing_tokens" => ( + 7, + &[ + (0, false, true), + (1, false, true), + (2, true, false), + (3, false, false), + (4, false, false), + (5, false, false), + (6, false, false), + ], + ), + "create_master_edition" => ( + 9, + &[ + (0, false, true), + (1, false, true), + (2, true, false), + (3, true, false), + (4, true, true), + (5, false, false), + (6, false, false), + (7, false, false), + (8, false, false), + ], + ), + "deprecated_mint_new_edition_from_master_edition_via_printing_token" => ( + 15, + &[ + (0, false, true), + (1, false, true), + (2, false, true), + (3, false, true), + (4, true, false), + (5, false, true), + (6, false, true), + (7, false, true), + (8, true, false), + (9, true, false), + (10, false, false), + (11, false, false), + (12, false, false), + (13, false, false), + (14, false, false), + ], + ), + "burn_nft" => ( + 6, + &[ + (0, false, true), + (1, true, true), + (2, false, true), + (3, false, true), + (4, false, true), + (5, false, false), + ], + ), + "burn_edition_nft" => ( + 10, + &[ + (0, false, true), + (1, true, true), + (2, false, true), + (3, false, false), + (4, false, true), + (5, false, false), + (6, false, true), + (7, false, true), + (8, false, true), + (9, false, false), + ], + ), + "freeze_delegated_account" | "thaw_delegated_account" => ( + 5, + &[ + (0, true, true), + (1, false, true), + (2, false, false), + (3, false, false), + (4, false, false), + ], + ), + "update_primary_sale_happened_via_token" => { + (3, &[(0, false, true), (1, true, false), (2, false, false)]) + }, + "sign_metadata" => (2, &[(0, false, true), (1, true, false)]), + "puff_metadata" => (1, &[(0, false, true)]), + "mint_new_edition_from_master_edition_via_token" => ( + 13, + &[ + (0, false, true), + (1, false, true), + (2, false, true), + (3, false, true), + (4, false, true), + (5, true, false), + (6, true, true), + (7, true, false), + (8, false, false), + (9, false, false), + (10, false, false), + (11, false, false), + (12, false, false), + ], + ), + "mint_new_edition_from_master_edition_via_vault_proxy" => ( + 16, + &[ + (0, false, true), + (1, false, true), + (2, false, true), + (3, false, true), + (4, false, true), + (5, true, false), + (6, true, true), + (7, true, false), + (8, false, false), + (9, false, false), + (10, false, false), + (11, false, false), + (12, false, false), + (13, false, false), + (14, false, false), + (15, false, false), + ], + ), + "convert_master_edition_v1_to_v2" => { + (3, &[(0, false, true), (1, false, true), (2, false, true)]) + }, + "create_master_edition_v3" => ( + 8, + &[ + (0, false, true), + (1, false, true), + (2, true, false), + (3, true, false), + (4, true, true), + (5, false, true), + (6, false, false), + (7, false, false), + ], + ), + "remove_creator_verification" => (2, &[(0, false, true), (1, true, false)]), + "set_token_standard" => (3, &[(0, false, true), (1, true, false), (2, false, false)]), + "create_metadata_account" | "create_metadata_account_v2" | "create_metadata_account_v3" => { + (6, &[(0, false, true), (1, false, false), (2, true, false), (3, true, true)]) + }, + "update_metadata_account" | "update_metadata_account_v2" => { + (2, &[(0, false, true), (1, true, false)]) + }, + "set_collection_size" => (3, &[(0, false, true), (1, true, true), (2, false, false)]), + "bubblegum_set_collection_size" => { + (4, &[(0, false, true), (1, true, false), (2, false, false), (3, true, false)]) + }, + "verify_collection" => ( + 6, + &[ + (0, false, true), + (1, true, true), + (2, true, true), + (3, false, false), + (4, false, false), + (5, false, false), + ], + ), + "unverify_collection" => ( + 5, + &[ + (0, false, true), + (1, true, true), + (2, false, false), + (3, false, false), + (4, false, false), + ], + ), + "set_and_verify_collection" => ( + 7, + &[ + (0, false, true), + (1, true, true), + (2, true, true), + (3, false, false), + (4, false, false), + (5, false, false), + (6, false, false), + ], + ), + "approve_collection_authority" => ( + 7, + &[ + (0, false, true), + (1, false, false), + (2, true, true), + (3, true, true), + (4, false, false), + (5, false, false), + (6, false, false), + ], + ), + "revoke_collection_authority" => ( + 5, + &[ + (0, false, true), + (1, false, true), + (2, true, true), + (3, false, false), + (4, false, false), + ], + ), + "approve_use_authority" => ( + 10, + &[ + (0, false, true), + (1, true, true), + (2, true, true), + (3, false, false), + (4, false, true), + (5, false, false), + (6, false, false), + (7, false, false), + (8, false, false), + (9, false, false), + ], + ), + "revoke_use_authority" => ( + 8, + &[ + (0, false, true), + (1, true, true), + (2, false, false), + (3, false, true), + (4, false, false), + (5, false, false), + (6, false, false), + (7, false, false), + ], + ), + "utilize" => ( + 9, + &[ + (0, false, true), + (1, false, true), + (2, false, true), + (3, true, true), + (4, false, false), + (5, false, false), + (6, false, false), + (7, false, false), + (8, false, false), + ], + ), + "verify_sized_collection_item" | "unverify_sized_collection_item" => ( + 6, + &[ + (0, false, true), + (1, true, false), + (2, true, true), + (3, false, false), + (4, false, true), + (5, false, false), + ], + ), + "set_and_verify_sized_collection_item" => ( + 7, + &[ + (0, false, true), + (1, true, false), + (2, true, true), + (3, false, false), + (4, false, false), + (5, false, true), + (6, false, false), + ], + ), + _ => return std::result::Result::Err(format!("unknown account contract for {code}")), + }; + if accounts.len() < required { + return std::result::Result::Err(format!( + "{code} requires at least {required} accounts, received {}", + accounts.len() + )); + } + for (index, signer, writable) in exact_flags { + let account = match accounts.get(*index) { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(format!("{code} is missing account {index}")); + }, + }; + if account.signer != *signer || account.writable != *writable { + return std::result::Result::Err(format!( + "{code} account {index} has invalid signer/writable flags" + )); + } + } + return std::result::Result::Ok(()); +} + +fn project_accounts(code: &str, accounts: &[ResolvedAccount]) -> serde_json::Value { + let roles: &[&str] = match code { + "create" => &[ + "metadata", + "master_edition", + "mint", + "authority", + "payer", + "update_authority", + "system_program", + "sysvar_instructions", + "spl_token_program", + ], + "mint" => &[ + "token", + "token_owner", + "metadata", + "master_edition", + "token_record", + "mint", + "authority", + "delegate_record", + "payer", + "system_program", + "sysvar_instructions", + "spl_token_program", + "spl_ata_program", + "authorization_rules_program", + "authorization_rules", + ], + "print" => &[ + "edition_metadata", + "edition", + "edition_mint", + "edition_token_account_owner", + "edition_token_account", + "edition_mint_authority", + "edition_token_record", + "master_edition", + "edition_marker_pda", + "payer", + "master_token_account_owner", + "master_token_account", + "master_metadata", + "update_authority", + "spl_token_program", + "spl_ata_program", + "sysvar_instructions", + "system_program", + ], + "update" => &[ + "authority", + "delegate_record", + "token", + "mint", + "metadata", + "edition", + "payer", + "system_program", + "sysvar_instructions", + "authorization_rules_program", + "authorization_rules", + ], + "use" => &[ + "authority", + "delegate_record", + "token", + "mint", + "metadata", + "edition", + "payer", + "system_program", + "sysvar_instructions", + "spl_token_program", + "authorization_rules_program", + "authorization_rules", + ], + "verify" | "unverify" => &[ + "authority", + "delegate_record", + "metadata", + "collection_mint", + "collection_metadata", + "collection_master_edition", + "system_program", + "sysvar_instructions", + ], + "resize" => { + &["metadata", "edition", "mint", "payer", "authority", "token", "system_program"] + }, + "migrate" => &[ + "metadata", + "edition", + "token", + "token_owner", + "mint", + "payer", + "authority", + "collection_metadata", + "delegate_record", + "token_record", + "system_program", + "sysvar_instructions", + "spl_token_program", + "authorization_rules_program", + "authorization_rules", + ], + "transfer" => &[ + "token", + "token_owner", + "destination_token", + "destination_owner", + "mint", + "metadata", + "edition", + "token_record", + "destination_token_record", + "authority", + "payer", + "system_program", + "sysvar_instructions", + "spl_token_program", + "spl_ata_program", + "authorization_rules_program", + "authorization_rules", + ], + "lock" | "unlock" => &[ + "authority", + "token_owner", + "token", + "mint", + "metadata", + "edition", + "token_record", + "payer", + "system_program", + "sysvar_instructions", + "spl_token_program", + "authorization_rules_program", + "authorization_rules", + ], + "burn" => &[ + "authority", + "collection_metadata", + "metadata", + "edition", + "mint", + "token", + "master_edition", + "master_edition_mint", + "master_edition_token", + "edition_marker", + "token_record", + "system_program", + "sysvar_instructions", + "spl_token_program", + ], + "close_accounts" => &["metadata", "edition", "mint", "authority", "destination"], + "create_escrow_account" => &[ + "escrow", + "metadata", + "mint", + "token_account", + "edition", + "payer", + "system_program", + "sysvar_instructions", + "authority", + ], + "close_escrow_account" => &[ + "escrow", + "metadata", + "mint", + "token_account", + "edition", + "payer", + "system_program", + "sysvar_instructions", + ], + "transfer_out_of_escrow" => &[ + "escrow", + "metadata", + "payer", + "attribute_mint", + "attribute_src", + "attribute_dst", + "escrow_mint", + "escrow_account", + "system_program", + "ata_program", + "token_program", + "sysvar_instructions", + "authority", + ], + "collect" => &["authority", "recipient"], + "deprecated_create_master_edition" => &[ + "edition", + "mint", + "printing_mint", + "one_time_printing_authorization_mint", + "update_authority", + "printing_mint_authority", + "mint_authority", + "metadata", + "payer", + "token_program", + "system_program", + "rent", + "one_time_printing_authorization_mint_authority", + ], + "deprecated_set_reservation_list" => &["master_edition", "reservation_list", "resource"], + "deprecated_create_reservation_list" => &[ + "reservation_list", + "payer", + "update_authority", + "master_edition", + "resource", + "metadata", + "system_program", + "rent", + ], + "deprecated_mint_printing_tokens_via_token" => &[ + "destination", + "token", + "one_time_printing_authorization_mint", + "printing_mint", + "burn_authority", + "metadata", + "master_edition", + "token_program", + "rent", + ], + "deprecated_mint_printing_tokens" => &[ + "destination", + "printing_mint", + "update_authority", + "metadata", + "master_edition", + "token_program", + "rent", + ], + "create_master_edition" => &[ + "edition", + "mint", + "update_authority", + "mint_authority", + "payer", + "metadata", + "token_program", + "system_program", + "rent", + ], + "deprecated_mint_new_edition_from_master_edition_via_printing_token" => &[ + "metadata", + "edition", + "master_edition", + "mint", + "mint_authority", + "printing_mint", + "master_token_account", + "edition_marker", + "burn_authority", + "payer", + "master_update_authority", + "master_metadata", + "token_program", + "system_program", + "rent", + "reservation_list", + ], + "burn_nft" => &[ + "metadata", + "owner", + "mint", + "token_account", + "master_edition_account", + "spl_token_program", + "collection_metadata", + ], + "burn_edition_nft" => &[ + "metadata", + "owner", + "print_edition_mint", + "master_edition_mint", + "print_edition_token_account", + "master_edition_token_account", + "master_edition_account", + "print_edition_account", + "edition_marker_account", + "spl_token_program", + ], + "freeze_delegated_account" | "thaw_delegated_account" => { + &["delegate", "token_account", "edition", "mint", "token_program"] + }, + "delegate" | "revoke" => &[ + "delegate_record", + "delegate", + "metadata", + "master_edition", + "token_record", + "mint", + "token", + "authority", + "payer", + "system_program", + "sysvar_instructions", + "spl_token_program", + "authorization_rules_program", + "authorization_rules", + ], + "update_primary_sale_happened_via_token" => &["metadata", "owner", "token_account"], + "sign_metadata" => &["metadata", "creator"], + "puff_metadata" => &["metadata"], + "mint_new_edition_from_master_edition_via_token" => &[ + "new_metadata", + "new_edition", + "master_edition", + "new_mint", + "edition_marker", + "new_mint_authority", + "payer", + "token_account_owner", + "token_account", + "new_metadata_update_authority", + "master_metadata", + "token_program", + "system_program", + "rent", + ], + "mint_new_edition_from_master_edition_via_vault_proxy" => &[ + "new_metadata", + "new_edition", + "master_edition", + "new_mint", + "edition_marker", + "new_mint_authority", + "payer", + "vault_authority", + "safety_deposit_store", + "safety_deposit_box", + "vault", + "new_metadata_update_authority", + "master_metadata", + "token_program", + "token_vault_program", + "system_program", + "rent", + ], + "convert_master_edition_v1_to_v2" => { + &["master_edition", "one_time_authorization_mint", "printing_mint"] + }, + "create_master_edition_v3" => &[ + "master_edition", + "mint", + "update_authority", + "mint_authority", + "payer", + "metadata", + "token_program", + "system_program", + "rent", + ], + "remove_creator_verification" => &["metadata", "creator"], + "set_token_standard" => &["metadata", "update_authority", "mint", "edition"], + "create_metadata_account" | "create_metadata_account_v2" | "create_metadata_account_v3" => { + &[ + "metadata", + "mint", + "mint_authority", + "payer", + "update_authority", + "system_program", + "rent", + ] + }, + "update_metadata_account" | "update_metadata_account_v2" => { + &["metadata", "update_authority"] + }, + "set_collection_size" => &[ + "collection_metadata", + "collection_authority", + "collection_mint", + "collection_authority_record", + ], + "bubblegum_set_collection_size" => &[ + "collection_metadata", + "collection_authority", + "collection_mint", + "bubblegum_signer", + "collection_authority_record", + ], + "verify_collection" => &[ + "metadata", + "collection_authority", + "payer", + "collection_mint", + "collection_metadata", + "collection_master_edition", + "collection_authority_record", + ], + "unverify_collection" => &[ + "metadata", + "collection_authority", + "collection_mint", + "collection_metadata", + "collection_master_edition", + "collection_authority_record", + ], + "set_and_verify_collection" => &[ + "metadata", + "collection_authority", + "payer", + "update_authority", + "collection_mint", + "collection_metadata", + "collection_master_edition", + "collection_authority_record", + ], + "approve_collection_authority" => &[ + "collection_authority_record", + "new_collection_authority", + "update_authority", + "payer", + "collection_metadata", + "collection_mint", + "system_program", + "rent", + ], + "revoke_collection_authority" => &[ + "collection_authority_record", + "delegate_authority", + "revoke_authority", + "collection_metadata", + "collection_mint", + ], + "approve_use_authority" => &[ + "use_authority_record", + "owner", + "payer", + "user", + "owner_token_account", + "metadata", + "mint", + "burner", + "token_program", + "system_program", + "rent", + ], + "revoke_use_authority" => &[ + "use_authority_record", + "owner", + "user", + "owner_token_account", + "mint", + "metadata", + "token_program", + "system_program", + "rent", + ], + "utilize" => &[ + "metadata", + "token_account", + "mint", + "use_authority", + "owner", + "token_program", + "ata_program", + "system_program", + "rent", + "use_authority_record", + "burner", + ], + "verify_sized_collection_item" | "unverify_sized_collection_item" => &[ + "metadata", + "collection_authority", + "payer", + "collection_mint", + "collection_metadata", + "collection_master_edition", + "collection_authority_record", + ], + "set_and_verify_sized_collection_item" => &[ + "metadata", + "collection_authority", + "payer", + "update_authority", + "collection_mint", + "collection_metadata", + "collection_master_edition", + "collection_authority_record", + ], + _ => &[], + }; + return serde_json::Value::Array( + accounts + .iter() + .enumerate() + .map(|(position, 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": roles.get(position).copied().unwrap_or("remaining_account"), + }); + }) + .collect(), + ); +} + +fn observation( + input: &crate::CoreInstructionReplayInput, + parsed: &crate::decoder::metadata::metaplex_token_metadata::ParsedInstruction, + accounts: serde_json::Value, + wire: &[u8], +) -> crate::DecodedObservation { + let wire_hash = hash(wire); + let committed = !input.transaction_failed; + let event = crate::DecodedProtocolEvent { + signature: crate::Signature(input.signature.clone()), + slot: crate::Slot(input.slot), + instruction_path: crate::InstructionPath(input.instruction_path.clone()), + program_id: crate::ProgramId(input.program_id.clone()), + protocol_code: crate::ProtocolCode( + crate::decoder::metadata::metaplex_token_metadata::SURFACE_CODE.to_string(), + ), + surface_code: crate::SurfaceCode( + crate::decoder::metadata::metaplex_token_metadata::SURFACE_CODE.to_string(), + ), + event_code: crate::EventCode(format!( + "{}.{}", + crate::decoder::metadata::metaplex_token_metadata::SURFACE_CODE, + parsed.code + )), + event_name: crate::EventName(parsed.code.to_string()), + event_family: crate::EventFamily::Metadata, + source_kind: if input.instruction_path.contains('/') { + crate::EventSourceKind::InnerInstruction + } else { + crate::EventSourceKind::Instruction + }, + confidence: crate::DecoderConfidence::ManualExact, + }; + return crate::DecodedObservation { + event_key: format!("metaplex_token_metadata:{}:0", parsed.code), + event, + payload_json: serde_json::json!({ + "eventVersion": crate::decoder::metadata::metaplex_token_metadata::EVENT_VERSION, + "programId": input.program_id, + "instruction": parsed.code, + "wireDiscriminator": parsed.discriminator, + "wireDiscriminatorHex": format!("{:02x}", parsed.discriminator), + "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), "suffixLengthBytes": 0, "complete": true }, + "provenance": { "sdk": "mpl-token-metadata", "sdkVersion": "5.1.1", "layout": "official_borsh", "externalMetadata": true, "token2022EmbeddedMetadata": false }, + }), + transaction_failed: input.transaction_failed, + transaction_error: input.transaction_err_json.clone(), + observation_committed: committed, + proof: crate::DecoderProof { + kind: crate::DecoderProofKind::Manual, + confidence: crate::DecoderConfidence::ManualExact, + evidence: std::vec![ + "mpl_token_metadata_5_1_1_generated_wire".to_string(), + format!("wire_discriminator:{}", parsed.discriminator), + format!("wire_sha256:{wire_hash}") + ], + }, + }; +} + +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::decoder::metadata::metaplex_token_metadata::DIAGNOSTIC_PREFIX_BYTES, + ); + let mut output = std::string::String::with_capacity(length.saturating_mul(2)); + for byte in &bytes[..length] { + output.push_str(format!("{byte:02x}").as_str()); + } + return output; +} + +fn failed( + entry_code: std::option::Option<&str>, + code: &str, + message: std::string::String, +) -> crate::DecoderExecutionResult { + return crate::DecoderExecutionResult { + status: crate::DecoderOutcomeStatus::Failed, + recognized_entry_code: entry_code.map(str::to_string), + observations: std::vec::Vec::new(), + diagnostics: std::vec![crate::DecoderDiagnostic { + code: code.to_string(), + message, + retriable: false + }], + }; +} diff --git a/kb-lib/src/lib.rs b/kb-lib/src/lib.rs index 00aba06..0f6b3eb 100644 --- a/kb-lib/src/lib.rs +++ b/kb-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-lib/src/lib.rs -// version: 10 +// version: 11 //! Consolidated decoder, executor, materializer and shared model library. #![warn(missing_docs)] @@ -291,6 +291,8 @@ pub(crate) use crate::decoder::spl_token_payload_tag; pub use crate::decoder::ELGAMAL_REGISTRY_ACCOUNT_LEN; /// Parsed SPL ElGamal public-key registry account. pub use crate::decoder::ElGamalRegistryState; +/// Exact decoder for the Metaplex Token Metadata program. +pub use crate::decoder::MetadataMetaplexTokenMetadataDecoder; /// Exact decoder for the SPL Associated Token Account program. pub use crate::decoder::SplAssociatedTokenAccountDecoder; /// Exact decoder for the SPL ElGamal registry program. diff --git a/kb-program-ids/README.md b/kb-program-ids/README.md index 70dd655..8eb7e0c 100644 --- a/kb-program-ids/README.md +++ b/kb-program-ids/README.md @@ -1,11 +1,11 @@ - + # kb-program-ids `kb-program-ids` est la source unique des identifiants de programmes Solana et des comptes natifs connus utilisés par le workspace. -La tranche `0.1.0-pre.004` expose les 18 surfaces exécutables nécessaires au décodeur Solana Core, les comptes natifs associés utilisés par leurs contrats et l’identifiant SPL Token classique employé par les tests de frontière. La tranche `0.1.0-pre.005` ajoute les trois identifiants SPL Memo v1, v3 et v4 sans les classer parmi les surfaces natives. `0.1.0-pre.006` active l’identifiant SPL Token classique dans un décodeur concret tout en le maintenant hors des surfaces natives. `0.1.0-pre.007` ajoute les identifiants SPL Associated Token Account et Token‑2022 nécessaires à la frontière ATA. `0.1.0-pre.008` ajoute le Program ID indépendant du registre ElGamal de Token‑2022, toujours hors des surfaces natives. +La tranche `0.1.0-pre.004` expose les 18 surfaces exécutables nécessaires au décodeur Solana Core, les comptes natifs associés utilisés par leurs contrats et l’identifiant SPL Token classique employé par les tests de frontière. La tranche `0.1.0-pre.005` ajoute les trois identifiants SPL Memo v1, v3 et v4 sans les classer parmi les surfaces natives. `0.1.0-pre.006` active l’identifiant SPL Token classique dans un décodeur concret tout en le maintenant hors des surfaces natives. `0.1.0-pre.007` ajoute les identifiants SPL Associated Token Account et Token‑2022 nécessaires à la frontière ATA. `0.1.0-pre.008` ajoute le Program ID indépendant du registre ElGamal de Token‑2022. `0.1.0-pre.009` ajoute le Program ID indépendant de Metaplex Token Metadata. Ces programmes restent hors des surfaces natives. `constants.rs` reste l’unique source des adresses et des tableaux statiques. `programs.rs` porte `ProgramIdEntry`, ses méthodes et toutes les fonctions de consultation. `lib.rs` se limite aux diff --git a/kb-program-ids/src/constants.rs b/kb-program-ids/src/constants.rs index c97753c..c4229a4 100644 --- a/kb-program-ids/src/constants.rs +++ b/kb-program-ids/src/constants.rs @@ -1,5 +1,5 @@ // file: kb-program-ids/src/constants.rs -// version: 4 +// version: 5 //! Canonical Solana program and well-known account identifiers. @@ -27,6 +27,9 @@ pub const INCINERATOR_PROGRAM_ID: &str = "1nc1nerator111111111111111111111111111 pub const LOADER_V4_PROGRAM_ID: &str = "LoaderV411111111111111111111111111111111111"; /// Native Loader program identifier. pub const NATIVE_LOADER_PROGRAM_ID: &str = "NativeLoader1111111111111111111111111111111"; +/// Metaplex Token Metadata program identifier. +pub const METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID: &str = + "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"; /// Secp256k1 precompile program identifier. pub const SECP256K1_PROGRAM_ID: &str = "KeccakSecp256k11111111111111111111111111111"; /// Secp256r1 precompile program identifier. @@ -198,6 +201,10 @@ pub(crate) const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[ name: "incinerator", address: crate::INCINERATOR_PROGRAM_ID, }, + crate::ProgramIdEntry { + name: "metadata_metaplex_token_metadata", + address: crate::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, + }, crate::ProgramIdEntry { name: "loader_v4", address: crate::LOADER_V4_PROGRAM_ID, diff --git a/kb-program-ids/src/lib.rs b/kb-program-ids/src/lib.rs index 4a998a7..514743e 100644 --- a/kb-program-ids/src/lib.rs +++ b/kb-program-ids/src/lib.rs @@ -1,5 +1,5 @@ // file: kb-program-ids/src/lib.rs -// version: 7 +// version: 8 #![forbid(unsafe_code)] #![deny(unreachable_pub)] @@ -39,6 +39,8 @@ pub use constants::FEATURE_PROGRAM_ID; pub use constants::INCINERATOR_PROGRAM_ID; /// Loader version 4 program identifier. pub use constants::LOADER_V4_PROGRAM_ID; +/// Metaplex Token Metadata program identifier. +pub use constants::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID; /// Native Loader program identifier. pub use constants::NATIVE_LOADER_PROGRAM_ID; /// Secp256k1 precompile program identifier. diff --git a/kb-program-ids/src/programs.rs b/kb-program-ids/src/programs.rs index 883b272..b3533ec 100644 --- a/kb-program-ids/src/programs.rs +++ b/kb-program-ids/src/programs.rs @@ -1,5 +1,5 @@ // file: kb-program-ids/src/programs.rs -// version: 1 +// version: 2 //! Enumerable canonical program registry. @@ -65,7 +65,7 @@ mod tests { assert!(codes.insert(entry.code())); assert!(program_ids.insert(entry.program_id())); } - assert_eq!(crate::registered_program_ids().len(), 32); + assert_eq!(crate::registered_program_ids().len(), 33); } #[test] @@ -122,8 +122,8 @@ mod tests { } #[test] - fn spl_primitives_are_registered_without_joining_native_surfaces() { - let spl_program_ids = [ + fn protocol_primitives_are_registered_without_joining_native_surfaces() { + let protocol_program_ids = [ crate::SPL_MEMO_V1_PROGRAM_ID, crate::SPL_MEMO_V3_PROGRAM_ID, crate::SPL_MEMO_V4_PROGRAM_ID, @@ -131,8 +131,9 @@ mod tests { crate::SPL_TOKEN_2022_PROGRAM_ID, crate::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID, crate::ASSOCIATED_TOKEN_PROGRAM_ID, + crate::METADATA_METAPLEX_TOKEN_METADATA_PROGRAM_ID, ]; - for program_id in spl_program_ids { + for program_id in protocol_program_ids { assert!(crate::find_registered_program_id(program_id).is_some()); assert!( !crate::native_program_ids()