v0.1.0-pre.007
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: README.md -->
|
<!-- file: README.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Khadhroony Bot3
|
# Khadhroony Bot3
|
||||||
|
|
||||||
@@ -53,4 +53,4 @@ cargo clippy --workspace --all-targets
|
|||||||
|
|
||||||
## Décodeurs consolidés
|
## Décodeurs consolidés
|
||||||
|
|
||||||
`0.1.0-pre.004` porte le décodeur Solana Core dans `kb-lib`. `0.1.0-pre.005` ajoute SPL Memo v1, v3 et v4. `0.1.0-pre.006` ajoute le décodeur SPL Token classique avec ses 28 tags publiés, son wire borné, ses formes d’autorité et son `Batch` structuré. Tous utilisent les mêmes contrats contextualisés, la même façade publique et aucune dépendance inverse vers `kb-store`.
|
`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. Tous utilisent les mêmes contrats contextualisés, la même façade publique et aucune dépendance inverse vers `kb-store`.
|
||||||
|
|||||||
18
ROADMAP.md
18
ROADMAP.md
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ROADMAP.md -->
|
<!-- file: ROADMAP.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# ROADMAP — khadhroony-bot3
|
# ROADMAP — khadhroony-bot3
|
||||||
|
|
||||||
@@ -71,5 +71,17 @@
|
|||||||
- [x] Conserver les bornes de `Batch`, ses chemins enfants et le refus des batches imbriqués.
|
- [x] Conserver les bornes de `Batch`, ses chemins enfants et le refus des batches imbriqués.
|
||||||
- [x] Porter le target de tracing vers `kb-lib.decoder.spl.token`.
|
- [x] Porter le target de tracing vers `kb-lib.decoder.spl.token`.
|
||||||
- [x] Rendre la contrainte `wincode 0.5.5` et `solana-wincode-varint 1.0.0` vérifiable par l’audit workspace.
|
- [x] Rendre la contrainte `wincode 0.5.5` et `solana-wincode-varint 1.0.0` vérifiable par l’audit workspace.
|
||||||
- [ ] Valider `kb-program-ids`, `kb-lib`, Clippy et le workspace avec Cargo sur la machine de développement.
|
- [x] Valider `kb-program-ids`, `kb-lib`, Clippy et le workspace avec Cargo sur la machine de développement.
|
||||||
- [ ] Poursuivre avec le décodeur SPL Associated Token Account après validation.
|
- [x] Poursuivre avec le décodeur SPL Associated Token Account après validation.
|
||||||
|
|
||||||
|
### 0.1.0-pre.007 — Décodeur SPL Associated Token Account dans `kb-lib`
|
||||||
|
|
||||||
|
- [x] Remplacer le scaffold `kb-lib::decoder::spl::associated_token_account` par le décodeur exact validé de bot2.
|
||||||
|
- [x] Conserver `Create`, `CreateIdempotent`, `RecoverNested` et la forme historique `Create` à payload vide.
|
||||||
|
- [x] Préserver l’ordre des comptes, les flags, doublons, chemins outer/inner et statuts non commités.
|
||||||
|
- [x] Valider les PDA canoniques `[wallet, token_program, mint]` sans remplacer les adresses observées.
|
||||||
|
- [x] Distinguer explicitement SPL Token classique et Token‑2022 sans décoder leurs états ni extensions.
|
||||||
|
- [x] Conserver les dix tests différentiels et la matrice machine-readable ATA.
|
||||||
|
- [x] Porter le target de tracing vers `kb-lib.decoder.spl.associated_token_account`.
|
||||||
|
- [ ] Valider `kb-program-ids`, les 196 tests de `kb-lib`, Clippy et le workspace avec Cargo sur la machine de développement.
|
||||||
|
- [ ] Poursuivre avec les décodeurs Token‑2022 et registre ElGamal après validation.
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
"kb_app_demo",
|
"kb_app_demo",
|
||||||
"kb_decoder_metadata_metaplex_token_metadata",
|
"kb_decoder_metadata_metaplex_token_metadata",
|
||||||
"kb-lib.decoder.solana.core",
|
"kb-lib.decoder.solana.core",
|
||||||
"kb_decoder_spl_associated_token_account",
|
"kb-lib.decoder.spl.associated_token_account",
|
||||||
"kb_decoder_spl_elgamal_registry",
|
"kb_decoder_spl_elgamal_registry",
|
||||||
"kb-lib.decoder.spl.memo",
|
"kb-lib.decoder.spl.memo",
|
||||||
"kb-lib.decoder.spl.token",
|
"kb-lib.decoder.spl.token",
|
||||||
@@ -225,42 +225,42 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_kb_decoder_spl_associated_token_account_debug",
|
"name": "file_kb_lib_decoder_spl_associated_token_account_debug",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sink": "file",
|
"sink": "file",
|
||||||
"level": "debug",
|
"level": "debug",
|
||||||
"path": "logs/devnet/kb_decoder_spl_associated_token_account/debug.log",
|
"path": "logs/devnet/kb_lib_decoder_spl_associated_token_account/debug.log",
|
||||||
"rotation": "daily",
|
"rotation": "daily",
|
||||||
"format": "human",
|
"format": "human",
|
||||||
"ansi": false,
|
"ansi": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
"kb_decoder_spl_associated_token_account"
|
"kb-lib.decoder.spl.associated_token_account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_kb_decoder_spl_associated_token_account_info",
|
"name": "file_kb_lib_decoder_spl_associated_token_account_info",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sink": "file",
|
"sink": "file",
|
||||||
"level": "info",
|
"level": "info",
|
||||||
"path": "logs/devnet/kb_decoder_spl_associated_token_account/info.log",
|
"path": "logs/devnet/kb_lib_decoder_spl_associated_token_account/info.log",
|
||||||
"rotation": "daily",
|
"rotation": "daily",
|
||||||
"format": "human",
|
"format": "human",
|
||||||
"ansi": false,
|
"ansi": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
"kb_decoder_spl_associated_token_account"
|
"kb-lib.decoder.spl.associated_token_account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_kb_decoder_spl_associated_token_account_error",
|
"name": "file_kb_lib_decoder_spl_associated_token_account_error",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sink": "file",
|
"sink": "file",
|
||||||
"level": "error",
|
"level": "error",
|
||||||
"path": "logs/devnet/kb_decoder_spl_associated_token_account/error.jsonl",
|
"path": "logs/devnet/kb_lib_decoder_spl_associated_token_account/error.jsonl",
|
||||||
"rotation": "daily",
|
"rotation": "daily",
|
||||||
"format": "json",
|
"format": "json",
|
||||||
"ansi": false,
|
"ansi": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
"kb_decoder_spl_associated_token_account"
|
"kb-lib.decoder.spl.associated_token_account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1626,7 +1626,7 @@
|
|||||||
"kb_app_demo",
|
"kb_app_demo",
|
||||||
"kb_decoder_metadata_metaplex_token_metadata",
|
"kb_decoder_metadata_metaplex_token_metadata",
|
||||||
"kb-lib.decoder.solana.core",
|
"kb-lib.decoder.solana.core",
|
||||||
"kb_decoder_spl_associated_token_account",
|
"kb-lib.decoder.spl.associated_token_account",
|
||||||
"kb_decoder_spl_elgamal_registry",
|
"kb_decoder_spl_elgamal_registry",
|
||||||
"kb-lib.decoder.spl.memo",
|
"kb-lib.decoder.spl.memo",
|
||||||
"kb-lib.decoder.spl.token",
|
"kb-lib.decoder.spl.token",
|
||||||
@@ -1827,42 +1827,42 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_kb_decoder_spl_associated_token_account_debug",
|
"name": "file_kb_lib_decoder_spl_associated_token_account_debug",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sink": "file",
|
"sink": "file",
|
||||||
"level": "debug",
|
"level": "debug",
|
||||||
"path": "logs/mainnet_research/kb_decoder_spl_associated_token_account/debug.log",
|
"path": "logs/mainnet_research/kb_lib_decoder_spl_associated_token_account/debug.log",
|
||||||
"rotation": "daily",
|
"rotation": "daily",
|
||||||
"format": "human",
|
"format": "human",
|
||||||
"ansi": false,
|
"ansi": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
"kb_decoder_spl_associated_token_account"
|
"kb-lib.decoder.spl.associated_token_account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_kb_decoder_spl_associated_token_account_info",
|
"name": "file_kb_lib_decoder_spl_associated_token_account_info",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sink": "file",
|
"sink": "file",
|
||||||
"level": "info",
|
"level": "info",
|
||||||
"path": "logs/mainnet_research/kb_decoder_spl_associated_token_account/info.log",
|
"path": "logs/mainnet_research/kb_lib_decoder_spl_associated_token_account/info.log",
|
||||||
"rotation": "daily",
|
"rotation": "daily",
|
||||||
"format": "human",
|
"format": "human",
|
||||||
"ansi": false,
|
"ansi": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
"kb_decoder_spl_associated_token_account"
|
"kb-lib.decoder.spl.associated_token_account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_kb_decoder_spl_associated_token_account_error",
|
"name": "file_kb_lib_decoder_spl_associated_token_account_error",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sink": "file",
|
"sink": "file",
|
||||||
"level": "error",
|
"level": "error",
|
||||||
"path": "logs/mainnet_research/kb_decoder_spl_associated_token_account/error.jsonl",
|
"path": "logs/mainnet_research/kb_lib_decoder_spl_associated_token_account/error.jsonl",
|
||||||
"rotation": "daily",
|
"rotation": "daily",
|
||||||
"format": "json",
|
"format": "json",
|
||||||
"ansi": false,
|
"ansi": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
"kb_decoder_spl_associated_token_account"
|
"kb-lib.decoder.spl.associated_token_account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -3428,7 +3428,7 @@
|
|||||||
"kb_app_demo",
|
"kb_app_demo",
|
||||||
"kb_decoder_metadata_metaplex_token_metadata",
|
"kb_decoder_metadata_metaplex_token_metadata",
|
||||||
"kb-lib.decoder.solana.core",
|
"kb-lib.decoder.solana.core",
|
||||||
"kb_decoder_spl_associated_token_account",
|
"kb-lib.decoder.spl.associated_token_account",
|
||||||
"kb_decoder_spl_elgamal_registry",
|
"kb_decoder_spl_elgamal_registry",
|
||||||
"kb-lib.decoder.spl.memo",
|
"kb-lib.decoder.spl.memo",
|
||||||
"kb-lib.decoder.spl.token",
|
"kb-lib.decoder.spl.token",
|
||||||
@@ -3629,42 +3629,42 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_kb_decoder_spl_associated_token_account_debug",
|
"name": "file_kb_lib_decoder_spl_associated_token_account_debug",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sink": "file",
|
"sink": "file",
|
||||||
"level": "debug",
|
"level": "debug",
|
||||||
"path": "logs/mainnet/kb_decoder_spl_associated_token_account/debug.log",
|
"path": "logs/mainnet/kb_lib_decoder_spl_associated_token_account/debug.log",
|
||||||
"rotation": "daily",
|
"rotation": "daily",
|
||||||
"format": "human",
|
"format": "human",
|
||||||
"ansi": false,
|
"ansi": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
"kb_decoder_spl_associated_token_account"
|
"kb-lib.decoder.spl.associated_token_account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_kb_decoder_spl_associated_token_account_info",
|
"name": "file_kb_lib_decoder_spl_associated_token_account_info",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sink": "file",
|
"sink": "file",
|
||||||
"level": "info",
|
"level": "info",
|
||||||
"path": "logs/mainnet/kb_decoder_spl_associated_token_account/info.log",
|
"path": "logs/mainnet/kb_lib_decoder_spl_associated_token_account/info.log",
|
||||||
"rotation": "daily",
|
"rotation": "daily",
|
||||||
"format": "human",
|
"format": "human",
|
||||||
"ansi": false,
|
"ansi": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
"kb_decoder_spl_associated_token_account"
|
"kb-lib.decoder.spl.associated_token_account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "file_kb_decoder_spl_associated_token_account_error",
|
"name": "file_kb_lib_decoder_spl_associated_token_account_error",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"sink": "file",
|
"sink": "file",
|
||||||
"level": "error",
|
"level": "error",
|
||||||
"path": "logs/mainnet/kb_decoder_spl_associated_token_account/error.jsonl",
|
"path": "logs/mainnet/kb_lib_decoder_spl_associated_token_account/error.jsonl",
|
||||||
"rotation": "daily",
|
"rotation": "daily",
|
||||||
"format": "json",
|
"format": "json",
|
||||||
"ansi": false,
|
"ansi": false,
|
||||||
"targets": [
|
"targets": [
|
||||||
"kb_decoder_spl_associated_token_account"
|
"kb-lib.decoder.spl.associated_token_account"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -150,10 +150,11 @@ instruction.
|
|||||||
### SPL Associated Token Account
|
### SPL Associated Token Account
|
||||||
|
|
||||||
Le catalogue workspace déclare `spl-associated-token-account-interface ^2.0` avec la feature
|
Le catalogue workspace déclare `spl-associated-token-account-interface ^2.0` avec la feature
|
||||||
`borsh`. Cargo a résolu la version `2.0.0` pendant le jalon `0.4.5`. Les crates
|
`borsh`. Cargo a résolu la version `2.0.0` pendant le jalon bot2 `0.4.5`. Depuis bot3
|
||||||
`kb_decoder_spl_associated_token_account` et `kb_executor_spl_associated_token_account` consomment
|
`0.1.0-pre.007`, `kb-lib::decoder::spl::associated_token_account` consomme réellement le Program
|
||||||
réellement l'enum, les builders, le Program ID et le helper officiel de dérivation ; le pipeline et
|
ID et le helper officiel de dérivation en production, puis l'enum et les builders dans ses tests
|
||||||
la démo consomment ensuite leurs contrats typés sans recopier l'interface.
|
différentiels. L'ancien exécuteur n'est pas encore porté ; le pipeline et la démo restent donc hors
|
||||||
|
de cette tranche.
|
||||||
|
|
||||||
L'interface publie exactement trois variantes Borsh actuelles : `Create` (`0`),
|
L'interface publie exactement trois variantes Borsh actuelles : `Create` (`0`),
|
||||||
`CreateIdempotent` (`1`) et `RecoverNested` (`2`). Le processor accepte aussi historiquement une
|
`CreateIdempotent` (`1`) et `RecoverNested` (`2`). Le processor accepte aussi historiquement une
|
||||||
@@ -164,10 +165,11 @@ classiques des ATA Token-2022 d'un même wallet et mint.
|
|||||||
|
|
||||||
Le décodeur conserve les comptes, flags, doublons, chemins outer/inner, transactions échouées,
|
Le décodeur conserve les comptes, flags, doublons, chemins outer/inner, transactions échouées,
|
||||||
adresses observées et dérivées, ainsi que les rôles distincts de `RecoverNested`. Les tests
|
adresses observées et dérivées, ainsi que les rôles distincts de `RecoverNested`. Les tests
|
||||||
différentiels comparent les trois builders et les vecteurs PDA au helper officiel. L'exécuteur
|
différentiels comparent les trois builders et les vecteurs PDA au helper officiel. Dans bot2,
|
||||||
utilise les mêmes builders pour les trois variantes, impose simulation et dry-run par défaut, puis
|
l'exécuteur validé utilisait les mêmes builders pour les trois variantes, imposait simulation et
|
||||||
laisse au pipeline stateful les contrôles de mint, owner, compte existant, rent, signataires et
|
dry-run par défaut, puis laissait au pipeline stateful les contrôles de mint, owner, compte
|
||||||
postconditions.
|
existant, rent, signataires et postconditions. Cette capacité reste une preuve de référence tant
|
||||||
|
que les modules exécuteur et pipeline correspondants ne sont pas portés dans bot3.
|
||||||
|
|
||||||
Les preuves Devnet du 16 juillet 2026 couvrent `CreateIdempotent` pour SPL Token classique et
|
Les preuves Devnet du 16 juillet 2026 couvrent `CreateIdempotent` pour SPL Token classique et
|
||||||
Token-2022, y compris création, réutilisation, replay post-exécution idempotent et ATA Token-2022
|
Token-2022, y compris création, réutilisation, replay post-exécution idempotent et ATA Token-2022
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
{
|
{
|
||||||
"file": "docs/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json",
|
"file": "docs/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json",
|
||||||
"version": 9,
|
"version": 10,
|
||||||
"schemaVersion": 1,
|
"schemaVersion": 1,
|
||||||
|
"bot3Migration": {
|
||||||
|
"target": "kb-lib.decoder.spl.associated_token_account",
|
||||||
|
"status": "ported_pending_operator_cargo_validation",
|
||||||
|
"prerelease": "0.1.0-pre.007",
|
||||||
|
"preservedDifferentialTests": 10,
|
||||||
|
"materializerPorted": false,
|
||||||
|
"executorPorted": false,
|
||||||
|
"pipelinePorted": false
|
||||||
|
},
|
||||||
"programId": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
|
"programId": "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL",
|
||||||
"systemProgramId": "11111111111111111111111111111111",
|
"systemProgramId": "11111111111111111111111111111111",
|
||||||
"supportedTokenPrograms": [
|
"supportedTokenPrograms": [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/TRACING_CONTRACT.md -->
|
<!-- file: docs/TRACING_CONTRACT.md -->
|
||||||
<!-- version: 15 -->
|
<!-- version: 16 -->
|
||||||
|
|
||||||
# Contrat de tracing par crate et composant
|
# Contrat de tracing par crate et composant
|
||||||
|
|
||||||
@@ -57,6 +57,7 @@ Les targets déjà migrés vers les noms canoniques bot3 sont :
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
kb-lib.decoder.solana.core
|
kb-lib.decoder.solana.core
|
||||||
|
kb-lib.decoder.spl.associated_token_account
|
||||||
kb-lib.decoder.spl.memo
|
kb-lib.decoder.spl.memo
|
||||||
kb-lib.decoder.spl.token
|
kb-lib.decoder.spl.token
|
||||||
kb-logging
|
kb-logging
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: kb-lib/Cargo.toml
|
# file: kb-lib/Cargo.toml
|
||||||
# version: 5
|
# version: 6
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "kb-lib"
|
name = "kb-lib"
|
||||||
@@ -25,6 +25,7 @@ solana-stake-interface.workspace = true
|
|||||||
solana-system-interface.workspace = true
|
solana-system-interface.workspace = true
|
||||||
solana-vote-interface.workspace = true
|
solana-vote-interface.workspace = true
|
||||||
solana-zk-elgamal-proof-interface.workspace = true
|
solana-zk-elgamal-proof-interface.workspace = true
|
||||||
|
spl-associated-token-account-interface.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
ts-rs.workspace = true
|
ts-rs.workspace = true
|
||||||
wincode.workspace = true
|
wincode.workspace = true
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: kb-lib/README.md -->
|
<!-- file: kb-lib/README.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# kb-lib
|
# kb-lib
|
||||||
|
|
||||||
@@ -43,11 +43,25 @@ cumulés et interdit les batches imbriqués. Le décodeur n’invente ni mint, n
|
|||||||
final ; les validations `M/N`, soldes et autorités existantes restent stateful. La matrice
|
final ; les validations `M/N`, soldes et autorités existantes restent stateful. La matrice
|
||||||
normative est `docs/SPL_TOKEN_MATRIX.json`.
|
normative est `docs/SPL_TOKEN_MATRIX.json`.
|
||||||
|
|
||||||
|
## Décodeur SPL Associated Token Account
|
||||||
|
|
||||||
|
`SplAssociatedTokenAccountDecoder` couvre exclusivement
|
||||||
|
`ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL`. Il reconnaît les trois variantes publiées
|
||||||
|
`Create`, `CreateIdempotent` et `RecoverNested`, ainsi que l’encodage historique vide de `Create`.
|
||||||
|
|
||||||
|
Le décodeur conserve les comptes dans leur ordre original, leurs flags, doublons, chemins
|
||||||
|
outer/inner et le statut de transaction. Il dérive les PDA avec l’ordre canonique
|
||||||
|
`[wallet, token_program, mint]`, conserve simultanément l’adresse observée et l’adresse attendue,
|
||||||
|
puis expose tout écart comme diagnostic. SPL Token classique et Token‑2022 sont distingués par leur
|
||||||
|
Program ID sans reconstruire leur état ni interpréter leurs extensions. La matrice normative est
|
||||||
|
`docs/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json`.
|
||||||
|
|
||||||
## API publique utile
|
## API publique utile
|
||||||
|
|
||||||
- `SolanaCoreDecoder` : décodeur concret natif ;
|
- `SolanaCoreDecoder` : décodeur concret natif ;
|
||||||
- `SplMemoDecoder` : décodeur exact des trois générations SPL Memo ;
|
- `SplMemoDecoder` : décodeur exact des trois générations SPL Memo ;
|
||||||
- `SplTokenDecoder` : décodeur exact du programme SPL Token classique ;
|
- `SplTokenDecoder` : décodeur exact du programme SPL Token classique ;
|
||||||
|
- `SplAssociatedTokenAccountDecoder` : décodeur exact du programme Associated Token Account ;
|
||||||
- `InstructionDecoder` : contrat de reconnaissance, couverture et décodage contextualisé ;
|
- `InstructionDecoder` : contrat de reconnaissance, couverture et décodage contextualisé ;
|
||||||
- `ProtocolDecoder` : contrat de compatibilité avec les observations historiques ;
|
- `ProtocolDecoder` : contrat de compatibilité avec les observations historiques ;
|
||||||
- `CoreInstructionReplayInput` : input source-neutral produit par l’extraction core ;
|
- `CoreInstructionReplayInput` : input source-neutral produit par l’extraction core ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-lib/src/decoder.rs
|
// file: kb-lib/src/decoder.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Consolidated decoder modules.
|
//! Consolidated decoder modules.
|
||||||
|
|
||||||
@@ -33,6 +33,24 @@ pub mod vesting;
|
|||||||
pub mod wallet;
|
pub mod wallet;
|
||||||
pub mod weighted;
|
pub mod weighted;
|
||||||
|
|
||||||
|
/// Current stable Associated Token Account decoded event contract version.
|
||||||
|
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION;
|
||||||
|
/// Exact Associated Token Account instructions published by the official interface.
|
||||||
|
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES;
|
||||||
|
/// Maximum retained account-key text length for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES;
|
||||||
|
/// Maximum retained instruction account count for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS;
|
||||||
|
/// Maximum retained semantic diagnostic count for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS;
|
||||||
|
/// Maximum retained Associated Token Account instruction payload size.
|
||||||
|
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES;
|
||||||
|
/// Maximum retained transaction account-key count for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS;
|
||||||
|
/// Stable protocol and surface code for the Associated Token Account program.
|
||||||
|
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE;
|
||||||
|
/// Canonical tracing target for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::spl::SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET;
|
||||||
/// Maximum payload prefix retained for bounded diagnostics.
|
/// Maximum payload prefix retained for bounded diagnostics.
|
||||||
pub(crate) use self::spl::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
|
pub(crate) use self::spl::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
|
||||||
/// Current stable Memo decoded event contract version.
|
/// Current stable Memo decoded event contract version.
|
||||||
@@ -65,6 +83,12 @@ pub(crate) use self::spl::SPL_TOKEN_MAX_INSTRUCTION_BYTES;
|
|||||||
pub(crate) use self::spl::SPL_TOKEN_SURFACE_CODE;
|
pub(crate) use self::spl::SPL_TOKEN_SURFACE_CODE;
|
||||||
/// Canonical tracing target for the classic SPL Token decoder.
|
/// Canonical tracing target for the classic SPL Token decoder.
|
||||||
pub(crate) use self::spl::SPL_TOKEN_TRACING_TARGET;
|
pub(crate) use self::spl::SPL_TOKEN_TRACING_TARGET;
|
||||||
|
/// Decodes one bounded Associated Token Account instruction.
|
||||||
|
pub(crate) use self::spl::spl_associated_token_account_decode;
|
||||||
|
/// Returns the exact entry matching an Associated Token Account wire.
|
||||||
|
pub(crate) use self::spl::spl_associated_token_account_entry;
|
||||||
|
/// Reads one bounded Associated Token Account payload.
|
||||||
|
pub(crate) use self::spl::spl_associated_token_account_payload;
|
||||||
/// Decodes one bounded SPL Memo instruction.
|
/// Decodes one bounded SPL Memo instruction.
|
||||||
pub(crate) use self::spl::spl_memo_decode;
|
pub(crate) use self::spl::spl_memo_decode;
|
||||||
/// Decodes one bounded classic SPL Token instruction.
|
/// Decodes one bounded classic SPL Token instruction.
|
||||||
@@ -74,6 +98,8 @@ pub(crate) use self::spl::spl_token_entry_for_tag;
|
|||||||
/// Returns the first byte of one retained classic SPL Token payload.
|
/// Returns the first byte of one retained classic SPL Token payload.
|
||||||
pub(crate) use self::spl::spl_token_payload_tag;
|
pub(crate) use self::spl::spl_token_payload_tag;
|
||||||
|
|
||||||
|
/// Exact decoder for the SPL Associated Token Account program.
|
||||||
|
pub use self::spl::SplAssociatedTokenAccountDecoder;
|
||||||
/// Exact decoder for the three registered SPL Memo generations.
|
/// Exact decoder for the three registered SPL Memo generations.
|
||||||
pub use self::spl::SplMemoDecoder;
|
pub use self::spl::SplMemoDecoder;
|
||||||
/// Exact decoder for the classic SPL Token program.
|
/// Exact decoder for the classic SPL Token program.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-lib/src/decoder/spl.rs
|
// file: kb-lib/src/decoder/spl.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! `spl` decoder family.
|
//! `spl` decoder family.
|
||||||
|
|
||||||
@@ -13,6 +13,30 @@ pub mod stake_pool;
|
|||||||
mod token;
|
mod token;
|
||||||
pub mod token_2022;
|
pub mod token_2022;
|
||||||
|
|
||||||
|
/// Current stable Associated Token Account decoded event contract version.
|
||||||
|
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION;
|
||||||
|
/// Exact Associated Token Account instructions published by the official interface.
|
||||||
|
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES;
|
||||||
|
/// Maximum retained account-key text length for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES;
|
||||||
|
/// Maximum retained instruction account count for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS;
|
||||||
|
/// Maximum retained semantic diagnostic count for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS;
|
||||||
|
/// Maximum retained Associated Token Account instruction payload size.
|
||||||
|
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES;
|
||||||
|
/// Maximum retained transaction account-key count for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS;
|
||||||
|
/// Stable protocol and surface code for the Associated Token Account program.
|
||||||
|
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE;
|
||||||
|
/// Canonical tracing target for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::associated_token_account::SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET;
|
||||||
|
/// Decodes one bounded Associated Token Account instruction.
|
||||||
|
pub(crate) use self::associated_token_account::spl_associated_token_account_decode;
|
||||||
|
/// Returns the exact entry matching an Associated Token Account wire.
|
||||||
|
pub(crate) use self::associated_token_account::spl_associated_token_account_entry;
|
||||||
|
/// Reads one bounded Associated Token Account payload.
|
||||||
|
pub(crate) use self::associated_token_account::spl_associated_token_account_payload;
|
||||||
/// Maximum payload prefix retained for bounded diagnostics.
|
/// Maximum payload prefix retained for bounded diagnostics.
|
||||||
pub(crate) use self::memo::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
|
pub(crate) use self::memo::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
|
||||||
/// Current stable Memo decoded event contract version.
|
/// Current stable Memo decoded event contract version.
|
||||||
@@ -54,6 +78,8 @@ pub(crate) use self::token::spl_token_entry_for_tag;
|
|||||||
/// Returns the first byte of one retained classic SPL Token payload.
|
/// Returns the first byte of one retained classic SPL Token payload.
|
||||||
pub(crate) use self::token::spl_token_payload_tag;
|
pub(crate) use self::token::spl_token_payload_tag;
|
||||||
|
|
||||||
|
/// Exact decoder for the SPL Associated Token Account program.
|
||||||
|
pub use self::associated_token_account::SplAssociatedTokenAccountDecoder;
|
||||||
/// Exact decoder for the three registered SPL Memo generations.
|
/// Exact decoder for the three registered SPL Memo generations.
|
||||||
pub use self::memo::SplMemoDecoder;
|
pub use self::memo::SplMemoDecoder;
|
||||||
/// Exact decoder for the classic SPL Token program.
|
/// Exact decoder for the classic SPL Token program.
|
||||||
|
|||||||
@@ -1,10 +1,36 @@
|
|||||||
// file: kb-lib/src/decoder/spl/associated_token_account.rs
|
// file: kb-lib/src/decoder/spl/associated_token_account.rs
|
||||||
// version: 1
|
// version: 2
|
||||||
|
|
||||||
//! Migration boundary for legacy crate `kb_decoder_spl_associated_token_account`.
|
//! Exact SPL Associated Token Account decoder component.
|
||||||
|
|
||||||
/// Legacy crate name retained for migration and compatibility tracking.
|
mod constants;
|
||||||
pub const LEGACY_CRATE: &str = "kb_decoder_spl_associated_token_account";
|
mod decoder;
|
||||||
|
mod wire;
|
||||||
|
|
||||||
/// Current porting status.
|
/// Current stable decoded event contract version.
|
||||||
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
|
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION;
|
||||||
|
/// Exact instructions published by the official interface.
|
||||||
|
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES;
|
||||||
|
/// Maximum retained account-key text length.
|
||||||
|
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES;
|
||||||
|
/// Maximum retained instruction account count.
|
||||||
|
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS;
|
||||||
|
/// Maximum retained semantic diagnostic count.
|
||||||
|
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS;
|
||||||
|
/// Maximum retained instruction payload size.
|
||||||
|
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES;
|
||||||
|
/// Maximum retained transaction account-key count.
|
||||||
|
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS;
|
||||||
|
/// Stable protocol and surface code for the Associated Token Account program.
|
||||||
|
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE;
|
||||||
|
/// Canonical tracing target for the Associated Token Account decoder.
|
||||||
|
pub(crate) use self::constants::SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET;
|
||||||
|
/// Decodes one bounded Associated Token Account instruction.
|
||||||
|
pub(crate) use self::wire::decode as spl_associated_token_account_decode;
|
||||||
|
/// Returns the exact entry matching an Associated Token Account wire.
|
||||||
|
pub(crate) use self::wire::entry as spl_associated_token_account_entry;
|
||||||
|
/// Reads one bounded Associated Token Account payload.
|
||||||
|
pub(crate) use self::wire::payload as spl_associated_token_account_payload;
|
||||||
|
|
||||||
|
/// Exact decoder for the SPL Associated Token Account program.
|
||||||
|
pub use self::decoder::SplAssociatedTokenAccountDecoder;
|
||||||
|
|||||||
28
kb-lib/src/decoder/spl/associated_token_account/constants.rs
Normal file
28
kb-lib/src/decoder/spl/associated_token_account/constants.rs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// file: kb-lib/src/decoder/spl/associated_token_account/constants.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Local constants for the `kb-lib` SPL Associated Token Account component.
|
||||||
|
|
||||||
|
/// Stable protocol and surface code for the Associated Token Account program.
|
||||||
|
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE: &str = "spl_associated_token_account";
|
||||||
|
/// Current stable decoded event contract version.
|
||||||
|
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION: u32 = 1;
|
||||||
|
/// Maximum retained instruction payload size.
|
||||||
|
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES: usize = 64;
|
||||||
|
/// Maximum retained instruction account count.
|
||||||
|
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS: usize = 64;
|
||||||
|
/// Maximum retained transaction account-key count.
|
||||||
|
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS: usize = 4_096;
|
||||||
|
/// Maximum retained account-key text length.
|
||||||
|
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES: usize = 128;
|
||||||
|
/// Maximum retained semantic diagnostic count.
|
||||||
|
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS: usize = 32;
|
||||||
|
/// Exact instructions published by `spl-associated-token-account-interface` 2.0.0.
|
||||||
|
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES: &[(u8, &str, bool)] = &[
|
||||||
|
(0, "create", false),
|
||||||
|
(1, "create_idempotent", false),
|
||||||
|
(2, "recover_nested", false),
|
||||||
|
];
|
||||||
|
/// Canonical tracing target for the Associated Token Account decoder.
|
||||||
|
pub(crate) const SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET: &str =
|
||||||
|
"kb-lib.decoder.spl.associated_token_account";
|
||||||
600
kb-lib/src/decoder/spl/associated_token_account/decoder.rs
Normal file
600
kb-lib/src/decoder/spl/associated_token_account/decoder.rs
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
// file: kb-lib/src/decoder/spl/associated_token_account/decoder.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Exact SPL Associated Token Account dispatch for the common decode pipeline.
|
||||||
|
|
||||||
|
const SURFACES: &[crate::DecoderSurface] = &[crate::DecoderSurface {
|
||||||
|
program_id: kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
surface_code: crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE,
|
||||||
|
priority: 100,
|
||||||
|
}];
|
||||||
|
|
||||||
|
const PROGRAM_IDS: &[&str] = &[kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID];
|
||||||
|
|
||||||
|
/// Exact decoder for the SPL Associated Token Account program.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct SplAssociatedTokenAccountDecoder;
|
||||||
|
|
||||||
|
impl crate::ProtocolDecoder for crate::SplAssociatedTokenAccountDecoder {
|
||||||
|
fn decoder_name(&self) -> &'static str {
|
||||||
|
return "kb_decoder_spl_associated_token_account";
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decoder_version(&self) -> &'static str {
|
||||||
|
return env!("CARGO_PKG_VERSION");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn program_ids(&self) -> &'static [&'static str] {
|
||||||
|
return PROGRAM_IDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supports_observation(
|
||||||
|
&self,
|
||||||
|
observation: &crate::ProgramObservation,
|
||||||
|
) -> crate::DecoderSupport {
|
||||||
|
return if crate::ProtocolDecoder::handles_program_id(self, &observation.program_id) {
|
||||||
|
crate::DecoderSupport::Yes
|
||||||
|
} else {
|
||||||
|
crate::DecoderSupport::No
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_observation(
|
||||||
|
&self,
|
||||||
|
_observation: &crate::ProgramObservation,
|
||||||
|
) -> kb_core::Result<std::vec::Vec<crate::DecodedProtocolEvent>> {
|
||||||
|
return std::result::Result::Ok(std::vec::Vec::new());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::InstructionDecoder for crate::SplAssociatedTokenAccountDecoder {
|
||||||
|
fn identity(&self) -> crate::DecoderIdentity {
|
||||||
|
return crate::DecoderIdentity {
|
||||||
|
name: crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_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<crate::DecoderCoverageDeclaration> {
|
||||||
|
return crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES
|
||||||
|
.iter()
|
||||||
|
.map(|(tag, code, historical)| {
|
||||||
|
return crate::DecoderCoverageDeclaration {
|
||||||
|
program_id: kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string(),
|
||||||
|
surface_code: std::option::Option::Some(
|
||||||
|
crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE.to_string(),
|
||||||
|
),
|
||||||
|
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
|
||||||
|
entry_code: (*code).to_string(),
|
||||||
|
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
|
||||||
|
historical: *historical,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recognize(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderRecognition {
|
||||||
|
if input.program_id != kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID {
|
||||||
|
return crate::DecoderRecognition::incompatible();
|
||||||
|
}
|
||||||
|
let wire = crate::spl_associated_token_account_payload(input).ok();
|
||||||
|
let entry = wire
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|bytes| return crate::spl_associated_token_account_entry(bytes));
|
||||||
|
return crate::DecoderRecognition::compatible(
|
||||||
|
entry.is_some(),
|
||||||
|
100,
|
||||||
|
std::option::Option::Some(crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE.to_string()),
|
||||||
|
entry.map(|(_, code, _)| return code.to_string()),
|
||||||
|
wire.and_then(|bytes| return bytes.first().map(|tag| return format!("{tag:02x}"))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
|
||||||
|
if input.program_id != kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID {
|
||||||
|
return crate::DecoderExecutionResult::unsupported(std::option::Option::None);
|
||||||
|
}
|
||||||
|
let result = crate::spl_associated_token_account_decode(input);
|
||||||
|
if matches!(
|
||||||
|
result.status,
|
||||||
|
crate::DecoderOutcomeStatus::Failed | crate::DecoderOutcomeStatus::Unsupported
|
||||||
|
) {
|
||||||
|
tracing::error!(
|
||||||
|
target: crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET,
|
||||||
|
action = "decode_failure",
|
||||||
|
signature = %input.signature,
|
||||||
|
slot = input.slot,
|
||||||
|
instruction_path = %input.instruction_path,
|
||||||
|
program_id = %input.program_id,
|
||||||
|
processor_name = crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE,
|
||||||
|
processor_version = env!("CARGO_PKG_VERSION"),
|
||||||
|
input_key = %input.replay_input_key,
|
||||||
|
result_status = ?result.status,
|
||||||
|
diagnostics = ?result.diagnostics,
|
||||||
|
"SPL Associated Token Account instruction was not decoded successfully"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tracing::debug!(
|
||||||
|
target: crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_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(),
|
||||||
|
"SPL Associated Token Account instruction decode completed"
|
||||||
|
);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
fn matrix() -> serde_json::Value {
|
||||||
|
return match serde_json::from_str(include_str!(
|
||||||
|
"../../../../../docs/SPL_ASSOCIATED_TOKEN_ACCOUNT_MATRIX.json"
|
||||||
|
)) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("SPL ATA matrix is invalid: {error}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn published_interface_and_matrix_variants_are_equal() {
|
||||||
|
let matrix = matrix();
|
||||||
|
let instructions = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => panic!("SPL ATA matrix has no instructions"),
|
||||||
|
};
|
||||||
|
let official = [
|
||||||
|
(
|
||||||
|
"create",
|
||||||
|
spl_associated_token_account_interface::instruction::AssociatedTokenAccountInstruction::Create,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"create_idempotent",
|
||||||
|
spl_associated_token_account_interface::instruction::AssociatedTokenAccountInstruction::CreateIdempotent,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"recover_nested",
|
||||||
|
spl_associated_token_account_interface::instruction::AssociatedTokenAccountInstruction::RecoverNested,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
assert_eq!(instructions.len(), official.len());
|
||||||
|
for (position, (name, variant)) in official.iter().enumerate() {
|
||||||
|
let encoded = match borsh::to_vec(variant) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
panic!("cannot encode official ATA variant: {error}")
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(encoded, vec![position as u8]);
|
||||||
|
assert_eq!(instructions[position]["name"], *name);
|
||||||
|
assert_eq!(instructions[position]["discriminant"], position as u64);
|
||||||
|
assert_eq!(instructions[position]["canonicalEncodingHex"], format!("{position:02x}"));
|
||||||
|
assert_eq!(instructions[position]["officialBuilder"]["available"], true);
|
||||||
|
}
|
||||||
|
assert_eq!(matrix["surfaceEquality"]["matrixVariantCount"], 3);
|
||||||
|
assert_eq!(matrix["surfaceEquality"]["officialInterfaceVariantCount"], 3);
|
||||||
|
let compiled = crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES
|
||||||
|
.iter()
|
||||||
|
.map(|(tag, code, _)| return (std::option::Option::Some(*tag), (*code).to_string()))
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
let declared =
|
||||||
|
crate::InstructionDecoder::coverage(&crate::SplAssociatedTokenAccountDecoder)
|
||||||
|
.iter()
|
||||||
|
.map(|entry| {
|
||||||
|
let tag = entry
|
||||||
|
.discriminator_hex
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|value| return u8::from_str_radix(value, 16).ok());
|
||||||
|
return (tag, entry.entry_code.clone());
|
||||||
|
})
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
assert_eq!(declared, compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn program_ids_accounts_and_pda_seed_order_are_explicit() {
|
||||||
|
let matrix = matrix();
|
||||||
|
assert_eq!(matrix["programId"], kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID);
|
||||||
|
assert_eq!(
|
||||||
|
spl_associated_token_account_interface::program::ID.to_string(),
|
||||||
|
kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID
|
||||||
|
);
|
||||||
|
assert_eq!(matrix["limits"]["createRequiredAccountCount"], 6);
|
||||||
|
assert_eq!(matrix["limits"]["recoverNestedRequiredAccountCount"], 7);
|
||||||
|
assert_eq!(
|
||||||
|
matrix["pdaDerivation"]["seedOrder"],
|
||||||
|
serde_json::json!([
|
||||||
|
"wallet_address_bytes[32]",
|
||||||
|
"token_program_id_bytes[32]",
|
||||||
|
"mint_address_bytes[32]"
|
||||||
|
])
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
matrix["supportedTokenPrograms"].as_array().map(std::vec::Vec::len),
|
||||||
|
std::option::Option::Some(2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn official_builders_match_matrix_wire_and_account_contracts() {
|
||||||
|
let funding = match "11111111111111111111111111111111".parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid funding fixture: {error}"),
|
||||||
|
};
|
||||||
|
let wallet = match "Vote111111111111111111111111111111111111111".parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid wallet fixture: {error}"),
|
||||||
|
};
|
||||||
|
let owner_mint = match "So11111111111111111111111111111111111111112".parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid owner mint fixture: {error}"),
|
||||||
|
};
|
||||||
|
let nested_mint = match "SysvarRent111111111111111111111111111111111".parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid nested mint fixture: {error}"),
|
||||||
|
};
|
||||||
|
let token_program = match kb_program_ids::SPL_TOKEN_PROGRAM_ID.parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid Token fixture: {error}"),
|
||||||
|
};
|
||||||
|
let create =
|
||||||
|
spl_associated_token_account_interface::instruction::create_associated_token_account(
|
||||||
|
&funding,
|
||||||
|
&wallet,
|
||||||
|
&owner_mint,
|
||||||
|
&token_program,
|
||||||
|
);
|
||||||
|
let idempotent = spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent(
|
||||||
|
&funding,
|
||||||
|
&wallet,
|
||||||
|
&owner_mint,
|
||||||
|
&token_program,
|
||||||
|
);
|
||||||
|
assert_eq!(create.data, vec![0]);
|
||||||
|
assert_eq!(idempotent.data, vec![1]);
|
||||||
|
assert_eq!(create.accounts.len(), 6);
|
||||||
|
assert_eq!(idempotent.accounts, create.accounts);
|
||||||
|
assert_eq!(create.accounts[0].pubkey, funding);
|
||||||
|
assert!(create.accounts[0].is_signer);
|
||||||
|
assert!(create.accounts[0].is_writable);
|
||||||
|
assert!(!create.accounts[1].is_signer);
|
||||||
|
assert!(create.accounts[1].is_writable);
|
||||||
|
assert_eq!(create.accounts[2].pubkey, wallet);
|
||||||
|
assert_eq!(create.accounts[3].pubkey, owner_mint);
|
||||||
|
assert_eq!(create.accounts[4].pubkey.to_string(), "11111111111111111111111111111111");
|
||||||
|
assert_eq!(create.accounts[5].pubkey, token_program);
|
||||||
|
let recover = spl_associated_token_account_interface::instruction::recover_nested(
|
||||||
|
&wallet,
|
||||||
|
&owner_mint,
|
||||||
|
&nested_mint,
|
||||||
|
&token_program,
|
||||||
|
);
|
||||||
|
assert_eq!(recover.data, vec![2]);
|
||||||
|
assert_eq!(recover.accounts.len(), 7);
|
||||||
|
assert!(recover.accounts[0].is_writable);
|
||||||
|
assert!(recover.accounts[2].is_writable);
|
||||||
|
assert_eq!(recover.accounts[5].pubkey, wallet);
|
||||||
|
assert!(recover.accounts[5].is_signer);
|
||||||
|
assert!(recover.accounts[5].is_writable);
|
||||||
|
assert_eq!(recover.accounts[6].pubkey, token_program);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn matrix_pda_vectors_match_the_official_helper() {
|
||||||
|
let matrix = matrix();
|
||||||
|
let vectors = match matrix["pdaDerivation"]["differentialVectors"].as_array() {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => panic!("SPL ATA matrix has no PDA vectors"),
|
||||||
|
};
|
||||||
|
for vector in vectors {
|
||||||
|
let wallet = match vector["wallet"].as_str().and_then(|value| return value.parse().ok())
|
||||||
|
{
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => panic!("invalid wallet PDA fixture"),
|
||||||
|
};
|
||||||
|
let mint = match vector["mint"].as_str().and_then(|value| return value.parse().ok()) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => panic!("invalid mint PDA fixture"),
|
||||||
|
};
|
||||||
|
let token_program =
|
||||||
|
match vector["tokenProgramId"].as_str().and_then(|value| return value.parse().ok())
|
||||||
|
{
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => panic!("invalid token program PDA fixture"),
|
||||||
|
};
|
||||||
|
let (address, bump) = spl_associated_token_account_interface::address::get_associated_token_address_and_bump_seed(
|
||||||
|
&wallet,
|
||||||
|
&mint,
|
||||||
|
&spl_associated_token_account_interface::program::ID,
|
||||||
|
&token_program,
|
||||||
|
);
|
||||||
|
assert_eq!(address.to_string(), vector["expectedAta"]);
|
||||||
|
assert_eq!(u64::from(bump), vector["expectedBump"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn replay(
|
||||||
|
wire: &[u8],
|
||||||
|
metas: &[(std::string::String, bool, bool)],
|
||||||
|
failed: bool,
|
||||||
|
path: &str,
|
||||||
|
) -> crate::CoreInstructionReplayInput {
|
||||||
|
use base64::Engine; // rust-rules: trait-import
|
||||||
|
|
||||||
|
let keys = metas
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(position, (key, signer, writable))| {
|
||||||
|
return serde_json::json!({
|
||||||
|
"accountIndex": position,
|
||||||
|
"accountKey": key,
|
||||||
|
"signer": signer,
|
||||||
|
"writable": writable,
|
||||||
|
"source": "static",
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
let accounts = metas
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(position, (key, _, _))| {
|
||||||
|
return serde_json::json!({
|
||||||
|
"position": position,
|
||||||
|
"accountIndex": position,
|
||||||
|
"accountKey": key,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
let result = crate::CoreInstructionReplayInput::new(
|
||||||
|
format!("signature:{path}"),
|
||||||
|
"signature",
|
||||||
|
42,
|
||||||
|
path,
|
||||||
|
kb_program_ids::ASSOCIATED_TOKEN_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(wire),
|
||||||
|
})),
|
||||||
|
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!("ATA replay fixture failed: {error}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_fixture(
|
||||||
|
token_program_text: &str,
|
||||||
|
idempotent: bool,
|
||||||
|
) -> (std::vec::Vec<u8>, std::vec::Vec<(std::string::String, bool, bool)>) {
|
||||||
|
let funding = match "Vote111111111111111111111111111111111111111".parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid funding fixture: {error}"),
|
||||||
|
};
|
||||||
|
let wallet = match "Stake11111111111111111111111111111111111111".parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid wallet fixture: {error}"),
|
||||||
|
};
|
||||||
|
let mint = match "So11111111111111111111111111111111111111112".parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid mint fixture: {error}"),
|
||||||
|
};
|
||||||
|
let token_program = match token_program_text.parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid token program fixture: {error}"),
|
||||||
|
};
|
||||||
|
let instruction = if idempotent {
|
||||||
|
spl_associated_token_account_interface::instruction::create_associated_token_account_idempotent(
|
||||||
|
&funding,
|
||||||
|
&wallet,
|
||||||
|
&mint,
|
||||||
|
&token_program,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
spl_associated_token_account_interface::instruction::create_associated_token_account(
|
||||||
|
&funding,
|
||||||
|
&wallet,
|
||||||
|
&mint,
|
||||||
|
&token_program,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let metas = instruction
|
||||||
|
.accounts
|
||||||
|
.iter()
|
||||||
|
.map(|meta| return (meta.pubkey.to_string(), meta.is_signer, meta.is_writable))
|
||||||
|
.collect();
|
||||||
|
return (instruction.data, metas);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn exact_dispatch_and_coverage_are_compiled() {
|
||||||
|
assert_eq!(
|
||||||
|
crate::InstructionDecoder::coverage(&crate::SplAssociatedTokenAccountDecoder).len(),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
let observation = crate::ProgramObservation {
|
||||||
|
signature: crate::Signature("signature".to_string()),
|
||||||
|
slot: crate::Slot(1),
|
||||||
|
instruction_path: crate::InstructionPath("0".to_string()),
|
||||||
|
program_id: crate::ProgramId(kb_program_ids::ASSOCIATED_TOKEN_PROGRAM_ID.to_string()),
|
||||||
|
discriminator_8: std::option::Option::None,
|
||||||
|
data_len: 1,
|
||||||
|
accounts_len: 6,
|
||||||
|
failed: false,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
crate::ProtocolDecoder::supports_observation(
|
||||||
|
&crate::SplAssociatedTokenAccountDecoder,
|
||||||
|
&observation,
|
||||||
|
),
|
||||||
|
crate::DecoderSupport::Yes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn create_and_legacy_empty_create_decode_with_canonical_address() {
|
||||||
|
let (wire, metas) = create_fixture(kb_program_ids::SPL_TOKEN_PROGRAM_ID, false);
|
||||||
|
for candidate in [wire.as_slice(), &[]] {
|
||||||
|
let input = replay(candidate, metas.as_slice(), false, "0");
|
||||||
|
let result =
|
||||||
|
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
|
||||||
|
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["addresses"]["associatedTokenAccount"]["valid"],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.observations[0].payload_json["semanticDiagnostics"],
|
||||||
|
serde_json::json!([])
|
||||||
|
);
|
||||||
|
let expected_hash = if candidate.is_empty() {
|
||||||
|
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
} else {
|
||||||
|
"6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d"
|
||||||
|
};
|
||||||
|
assert_eq!(result.observations[0].payload_json["wire"]["sha256"], expected_hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn token_2022_inner_failed_idempotent_remains_an_uncommitted_intent() {
|
||||||
|
let (wire, metas) = create_fixture(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, true);
|
||||||
|
let input = replay(wire.as_slice(), metas.as_slice(), true, "2/1");
|
||||||
|
let result =
|
||||||
|
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
|
||||||
|
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||||
|
assert!(!result.observations[0].observation_committed);
|
||||||
|
assert_eq!(
|
||||||
|
result.observations[0].event.source_kind,
|
||||||
|
crate::EventSourceKind::InnerInstruction
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
result.observations[0].payload_json["addresses"]["tokenProgram"]["kind"],
|
||||||
|
"token_2022"
|
||||||
|
);
|
||||||
|
assert_eq!(result.observations[0].payload_json["tokenProgramExtensionsDecoded"], false);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recover_nested_preserves_mint_roles_and_validates_all_three_pdas() {
|
||||||
|
let wallet = match "Vote111111111111111111111111111111111111111".parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid wallet fixture: {error}"),
|
||||||
|
};
|
||||||
|
let owner_mint = match "So11111111111111111111111111111111111111112".parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid owner mint fixture: {error}"),
|
||||||
|
};
|
||||||
|
let nested_mint = match "SysvarRent111111111111111111111111111111111".parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid nested mint fixture: {error}"),
|
||||||
|
};
|
||||||
|
let token_program = match kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID.parse() {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("invalid token program fixture: {error}"),
|
||||||
|
};
|
||||||
|
let instruction = spl_associated_token_account_interface::instruction::recover_nested(
|
||||||
|
&wallet,
|
||||||
|
&owner_mint,
|
||||||
|
&nested_mint,
|
||||||
|
&token_program,
|
||||||
|
);
|
||||||
|
let metas = instruction
|
||||||
|
.accounts
|
||||||
|
.iter()
|
||||||
|
.map(|meta| return (meta.pubkey.to_string(), meta.is_signer, meta.is_writable))
|
||||||
|
.collect::<std::vec::Vec<_>>();
|
||||||
|
let input = replay(instruction.data.as_slice(), metas.as_slice(), false, "1/0");
|
||||||
|
let result =
|
||||||
|
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
|
||||||
|
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||||
|
let addresses = &result.observations[0].payload_json["addresses"];
|
||||||
|
assert_eq!(addresses["ownerMint"], owner_mint.to_string());
|
||||||
|
assert_eq!(addresses["nestedMint"], nested_mint.to_string());
|
||||||
|
for field in [
|
||||||
|
"ownerAssociatedTokenAccount",
|
||||||
|
"nestedAssociatedTokenAccount",
|
||||||
|
"walletNestedMintAssociatedTokenAccount",
|
||||||
|
] {
|
||||||
|
assert_eq!(addresses[field]["valid"], true);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
result.observations[0].payload_json["semanticDiagnostics"],
|
||||||
|
serde_json::json!([])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wrong_address_flags_duplicates_extra_and_missing_accounts_stay_decodable() {
|
||||||
|
let (wire, mut metas) = create_fixture(kb_program_ids::SPL_TOKEN_PROGRAM_ID, false);
|
||||||
|
metas[1].0 = metas[0].0.clone();
|
||||||
|
metas[0].1 = false;
|
||||||
|
metas.push(("SysvarC1ock11111111111111111111111111111111".to_string(), false, false));
|
||||||
|
let input = replay(wire.as_slice(), metas.as_slice(), false, "0");
|
||||||
|
let result =
|
||||||
|
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
|
||||||
|
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||||
|
let diagnostics = result.observations[0].payload_json["semanticDiagnostics"].as_array();
|
||||||
|
let diagnostics = match diagnostics {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => panic!("ATA diagnostics are not an array"),
|
||||||
|
};
|
||||||
|
for code in [
|
||||||
|
"unexpected_account_count",
|
||||||
|
"account_flags_mismatch",
|
||||||
|
"duplicate_account_key",
|
||||||
|
"associated_address_mismatch",
|
||||||
|
] {
|
||||||
|
assert!(diagnostics.iter().any(|value| return value["code"] == code));
|
||||||
|
}
|
||||||
|
|
||||||
|
metas.truncate(3);
|
||||||
|
let input = replay(wire.as_slice(), metas.as_slice(), false, "0");
|
||||||
|
let result =
|
||||||
|
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
|
||||||
|
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
|
||||||
|
assert!(
|
||||||
|
result.observations[0].payload_json["semanticDiagnostics"]
|
||||||
|
.as_array()
|
||||||
|
.is_some_and(|values| return values
|
||||||
|
.iter()
|
||||||
|
.any(|value| return value["code"] == "missing_required_account"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_and_suffixed_wire_fail_with_bounded_diagnostics() {
|
||||||
|
let (_, metas) = create_fixture(kb_program_ids::SPL_TOKEN_PROGRAM_ID, false);
|
||||||
|
for wire in [std::vec![9], std::vec![0, 0]] {
|
||||||
|
let input = replay(wire.as_slice(), metas.as_slice(), false, "0");
|
||||||
|
let result =
|
||||||
|
crate::InstructionDecoder::decode(&crate::SplAssociatedTokenAccountDecoder, &input);
|
||||||
|
assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed);
|
||||||
|
assert_eq!(result.observations.len(), 0);
|
||||||
|
assert_eq!(result.diagnostics.len(), 1);
|
||||||
|
assert!(result.diagnostics[0].message.len() < 256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
465
kb-lib/src/decoder/spl/associated_token_account/wire.rs
Normal file
465
kb-lib/src/decoder/spl/associated_token_account/wire.rs
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
// file: kb-lib/src/decoder/spl/associated_token_account/wire.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Bounded ATA wire, ordered account and canonical PDA decoding.
|
||||||
|
|
||||||
|
use base64::Engine; // rust-rules: trait-import
|
||||||
|
use sha2::Digest; // rust-rules: trait-import
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct Account {
|
||||||
|
position: usize,
|
||||||
|
account_index: u64,
|
||||||
|
key: std::string::String,
|
||||||
|
signer: bool,
|
||||||
|
writable: bool,
|
||||||
|
source: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn entry(bytes: &[u8]) -> std::option::Option<(u8, &'static str, bool)> {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return std::option::Option::Some((0, "create", true));
|
||||||
|
}
|
||||||
|
if bytes.len() != 1 {
|
||||||
|
return std::option::Option::None;
|
||||||
|
}
|
||||||
|
return crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find(|(tag, _, _)| return *tag == bytes[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn payload(
|
||||||
|
input: &crate::CoreInstructionReplayInput,
|
||||||
|
) -> kb_core::Result<std::vec::Vec<u8>> {
|
||||||
|
let value = 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(
|
||||||
|
"ATA instruction payload is not retained",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let encoded = match value.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(
|
||||||
|
"ATA payload does not contain dataBase64",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if encoded.len() > 92 {
|
||||||
|
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||||
|
"ATA base64 payload exceeds the bounded input limit",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let bytes = match base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||||
|
"ATA payload is not valid base64: {error}"
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if bytes.len() > crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES {
|
||||||
|
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||||
|
"ATA decoded payload exceeds 64 bytes",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn decode(input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
|
||||||
|
let bytes = match crate::spl_associated_token_account_payload(input) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return failed(std::option::Option::None, "ata_payload_unavailable", error.to_string());
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let instruction = match crate::spl_associated_token_account_entry(bytes.as_slice()) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
let code = if bytes.len() > 1 {
|
||||||
|
"malformed_ata_instruction"
|
||||||
|
} else {
|
||||||
|
"unknown_ata_instruction_tag"
|
||||||
|
};
|
||||||
|
return failed(
|
||||||
|
std::option::Option::None,
|
||||||
|
code,
|
||||||
|
format!("unrecognized ATA wire; payloadPrefixHex={}", hex(bytes.as_slice())),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let accounts = match accounts(input) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return failed(
|
||||||
|
std::option::Option::Some(instruction.1),
|
||||||
|
"ata_accounts_malformed",
|
||||||
|
error.to_string(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let diagnostics = diagnostics(instruction.1, accounts.as_slice());
|
||||||
|
let projected_accounts = project_accounts(instruction.1, accounts.as_slice());
|
||||||
|
let addresses = addresses(instruction.1, accounts.as_slice());
|
||||||
|
let hash = hash(bytes.as_slice());
|
||||||
|
let committed = !input.transaction_failed;
|
||||||
|
let confidence = crate::DecoderConfidence::ManualExact;
|
||||||
|
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::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE.to_string(),
|
||||||
|
),
|
||||||
|
surface_code: crate::SurfaceCode(
|
||||||
|
crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE.to_string(),
|
||||||
|
),
|
||||||
|
event_code: crate::EventCode(format!(
|
||||||
|
"{}.{}",
|
||||||
|
crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE,
|
||||||
|
instruction.1
|
||||||
|
)),
|
||||||
|
event_name: crate::EventName(instruction.1.to_string()),
|
||||||
|
event_family: crate::EventFamily::Lifecycle,
|
||||||
|
source_kind: if input.instruction_path.contains('/') {
|
||||||
|
crate::EventSourceKind::InnerInstruction
|
||||||
|
} else {
|
||||||
|
crate::EventSourceKind::Instruction
|
||||||
|
},
|
||||||
|
confidence,
|
||||||
|
};
|
||||||
|
let observation = crate::DecodedObservation {
|
||||||
|
event_key: format!("ata:{}:0", instruction.1),
|
||||||
|
event,
|
||||||
|
payload_json: serde_json::json!({
|
||||||
|
"eventVersion": crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION,
|
||||||
|
"programId": input.program_id,
|
||||||
|
"instruction": instruction.1,
|
||||||
|
"wireTag": instruction.0,
|
||||||
|
"wireTagHex": format!("{:02x}", instruction.0),
|
||||||
|
"wireEncoding": if bytes.is_empty() { "legacy_empty_create" } else { "borsh_enum_discriminant" },
|
||||||
|
"instructionPath": input.instruction_path,
|
||||||
|
"instructionLocation": if input.instruction_path.contains('/') { "inner" } else { "outer" },
|
||||||
|
"transactionSucceeded": !input.transaction_failed,
|
||||||
|
"committed": committed,
|
||||||
|
"accounts": projected_accounts,
|
||||||
|
"addresses": addresses,
|
||||||
|
"semanticDiagnostics": diagnostics,
|
||||||
|
"wire": {
|
||||||
|
"lengthBytes": bytes.len(),
|
||||||
|
"sha256": hash,
|
||||||
|
"prefixHex": hex(bytes.as_slice()),
|
||||||
|
"complete": true,
|
||||||
|
},
|
||||||
|
"tokenProgramExtensionsDecoded": false,
|
||||||
|
"stateSnapshotReconstructed": false,
|
||||||
|
}),
|
||||||
|
transaction_failed: input.transaction_failed,
|
||||||
|
transaction_error: input.transaction_err_json.clone(),
|
||||||
|
observation_committed: committed,
|
||||||
|
proof: crate::DecoderProof {
|
||||||
|
kind: crate::DecoderProofKind::Manual,
|
||||||
|
confidence,
|
||||||
|
evidence: std::vec![
|
||||||
|
"spl_associated_token_account_interface_2_0_0_and_processor_audit".to_string(),
|
||||||
|
format!("wire_sha256:{hash}"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return crate::DecoderExecutionResult {
|
||||||
|
status: crate::DecoderOutcomeStatus::Decoded,
|
||||||
|
recognized_entry_code: std::option::Option::Some(instruction.1.to_string()),
|
||||||
|
observations: std::vec![observation],
|
||||||
|
diagnostics: std::vec::Vec::new(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accounts(input: &crate::CoreInstructionReplayInput) -> kb_core::Result<std::vec::Vec<Account>> {
|
||||||
|
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(
|
||||||
|
"ATA instruction accounts must be a JSON array",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let 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(
|
||||||
|
"ATA transaction account keys must be a JSON array",
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if instruction_accounts.len() > crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS {
|
||||||
|
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||||
|
"ATA instruction account count exceeds 64",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if keys.len() > crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS {
|
||||||
|
return std::result::Result::Err(kb_core::Error::invalid_state(
|
||||||
|
"ATA transaction account key count exceeds 4096",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut output = std::vec::Vec::with_capacity(instruction_accounts.len());
|
||||||
|
for (position, value) in instruction_accounts.iter().enumerate() {
|
||||||
|
let index = match value.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!(
|
||||||
|
"ATA instruction account {position} has no accountIndex"
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let key = match value.get("accountKey").and_then(serde_json::Value::as_str) {
|
||||||
|
std::option::Option::Some(value)
|
||||||
|
if !value.is_empty()
|
||||||
|
&& value.len() <= crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES =>
|
||||||
|
{
|
||||||
|
value
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||||
|
"ATA instruction account {position} has no accountKey"
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let resolved = match keys.iter().find(|candidate| {
|
||||||
|
return candidate.get("accountIndex").and_then(serde_json::Value::as_u64)
|
||||||
|
== std::option::Option::Some(index);
|
||||||
|
}) {
|
||||||
|
std::option::Option::Some(value) => value,
|
||||||
|
std::option::Option::None => {
|
||||||
|
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||||
|
"ATA account index {index} is not resolved"
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if resolved.get("accountKey").and_then(serde_json::Value::as_str)
|
||||||
|
!= std::option::Option::Some(key)
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
|
||||||
|
"ATA account index {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!(
|
||||||
|
"ATA account index {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!(
|
||||||
|
"ATA account index {index} has no writable flag"
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
output.push(Account {
|
||||||
|
position,
|
||||||
|
account_index: index,
|
||||||
|
key: key.to_string(),
|
||||||
|
signer,
|
||||||
|
writable,
|
||||||
|
source: resolved.get("source").cloned().unwrap_or(serde_json::Value::Null),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn roles(code: &str) -> &'static [(&'static str, bool, bool)] {
|
||||||
|
return if code == "recover_nested" {
|
||||||
|
&[
|
||||||
|
("nested_associated_token_account", false, true),
|
||||||
|
("nested_mint", false, false),
|
||||||
|
("wallet_nested_mint_associated_token_account", false, true),
|
||||||
|
("owner_associated_token_account", false, false),
|
||||||
|
("owner_mint", false, false),
|
||||||
|
("wallet_owner", true, true),
|
||||||
|
("token_program", false, false),
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
&[
|
||||||
|
("funding_account", true, true),
|
||||||
|
("associated_token_account", false, true),
|
||||||
|
("wallet_owner", false, false),
|
||||||
|
("mint", false, false),
|
||||||
|
("system_program", false, false),
|
||||||
|
("token_program", false, false),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_accounts(code: &str, accounts: &[Account]) -> serde_json::Value {
|
||||||
|
let roles = roles(code);
|
||||||
|
return serde_json::Value::Array(accounts.iter().map(|account| return serde_json::json!({
|
||||||
|
"position": account.position,
|
||||||
|
"accountIndex": account.account_index,
|
||||||
|
"accountKey": account.key,
|
||||||
|
"signer": account.signer,
|
||||||
|
"writable": account.writable,
|
||||||
|
"source": account.source,
|
||||||
|
"role": roles.get(account.position).map(|value| return value.0).unwrap_or("extra_account"),
|
||||||
|
})).collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagnostics(code: &str, accounts: &[Account]) -> serde_json::Value {
|
||||||
|
let roles = roles(code);
|
||||||
|
let mut values = std::vec::Vec::new();
|
||||||
|
if accounts.len() != roles.len() {
|
||||||
|
values.push(serde_json::json!({"code":"unexpected_account_count","expected":roles.len(),"observed":accounts.len()}));
|
||||||
|
}
|
||||||
|
for (position, (role, signer, writable)) in roles.iter().enumerate() {
|
||||||
|
if let std::option::Option::Some(account) = accounts.get(position) {
|
||||||
|
if account.signer != *signer || account.writable != *writable {
|
||||||
|
values.push(serde_json::json!({"code":"account_flags_mismatch","position":position,"role":role,"expectedSigner":signer,"observedSigner":account.signer,"expectedWritable":writable,"observedWritable":account.writable}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
values.push(serde_json::json!({"code":"missing_required_account","position":position,"role":role}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for left in 0..accounts.len() {
|
||||||
|
for right in left.saturating_add(1)..accounts.len() {
|
||||||
|
if accounts[left].key == accounts[right].key {
|
||||||
|
values.push(serde_json::json!({"code":"duplicate_account_key","positions":[left,right],"accountKey":accounts[left].key}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let expected = addresses(code, accounts);
|
||||||
|
for field in [
|
||||||
|
"associatedTokenAccount",
|
||||||
|
"ownerAssociatedTokenAccount",
|
||||||
|
"nestedAssociatedTokenAccount",
|
||||||
|
"walletNestedMintAssociatedTokenAccount",
|
||||||
|
] {
|
||||||
|
if expected
|
||||||
|
.get(field)
|
||||||
|
.and_then(|value| return value.get("valid"))
|
||||||
|
.and_then(serde_json::Value::as_bool)
|
||||||
|
== std::option::Option::Some(false)
|
||||||
|
{
|
||||||
|
values.push(serde_json::json!({"code":"associated_address_mismatch","field":field,"observed":expected[field]["observed"],"expected":expected[field]["expected"]}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let token_position = if code == "recover_nested" { 6 } else { 5 };
|
||||||
|
if accounts.get(token_position).is_some_and(|account| {
|
||||||
|
return account.key != kb_program_ids::SPL_TOKEN_PROGRAM_ID
|
||||||
|
&& account.key != kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID;
|
||||||
|
}) {
|
||||||
|
values.push(serde_json::json!({"code":"unsupported_token_program","observed":accounts.get(token_position).map(|value| return value.key.clone())}));
|
||||||
|
}
|
||||||
|
if code != "recover_nested"
|
||||||
|
&& accounts
|
||||||
|
.get(4)
|
||||||
|
.is_some_and(|account| return account.key != "11111111111111111111111111111111")
|
||||||
|
{
|
||||||
|
values
|
||||||
|
.push(serde_json::json!({"code":"system_program_mismatch","observed":accounts[4].key}));
|
||||||
|
}
|
||||||
|
values.truncate(crate::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS);
|
||||||
|
return serde_json::Value::Array(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn addresses(code: &str, accounts: &[Account]) -> serde_json::Value {
|
||||||
|
let token_position = if code == "recover_nested" { 6 } else { 5 };
|
||||||
|
let token = accounts.get(token_position).map(|value| return value.key.as_str());
|
||||||
|
if code == "recover_nested" {
|
||||||
|
return serde_json::json!({
|
||||||
|
"tokenProgram": token_kind(token),
|
||||||
|
"ownerAssociatedTokenAccount": derived(accounts.get(5), accounts.get(4), token, accounts.get(3)),
|
||||||
|
"nestedAssociatedTokenAccount": derived(accounts.get(3), accounts.get(1), token, accounts.first()),
|
||||||
|
"walletNestedMintAssociatedTokenAccount": derived(accounts.get(5), accounts.get(1), token, accounts.get(2)),
|
||||||
|
"walletOwner": accounts.get(5).map(|value| return value.key.clone()),
|
||||||
|
"ownerMint": accounts.get(4).map(|value| return value.key.clone()),
|
||||||
|
"nestedMint": accounts.get(1).map(|value| return value.key.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return serde_json::json!({
|
||||||
|
"tokenProgram": token_kind(token),
|
||||||
|
"associatedTokenAccount": derived(accounts.get(2), accounts.get(3), token, accounts.get(1)),
|
||||||
|
"fundingAccount": accounts.first().map(|value| return value.key.clone()),
|
||||||
|
"walletOwner": accounts.get(2).map(|value| return value.key.clone()),
|
||||||
|
"mint": accounts.get(3).map(|value| return value.key.clone()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derived(
|
||||||
|
wallet: std::option::Option<&Account>,
|
||||||
|
mint: std::option::Option<&Account>,
|
||||||
|
token: std::option::Option<&str>,
|
||||||
|
observed: std::option::Option<&Account>,
|
||||||
|
) -> serde_json::Value {
|
||||||
|
let expected = match (wallet, mint, token) {
|
||||||
|
(
|
||||||
|
std::option::Option::Some(wallet),
|
||||||
|
std::option::Option::Some(mint),
|
||||||
|
std::option::Option::Some(token),
|
||||||
|
) => {
|
||||||
|
let wallet_key = wallet.key.parse();
|
||||||
|
let mint_key = mint.key.parse();
|
||||||
|
let token_key = token.parse();
|
||||||
|
match (wallet_key, mint_key, token_key) {
|
||||||
|
(std::result::Result::Ok(wallet_key), std::result::Result::Ok(mint_key), std::result::Result::Ok(token_key)) => std::option::Option::Some(spl_associated_token_account_interface::address::get_associated_token_address_with_program_id(&wallet_key, &mint_key, &token_key).to_string()),
|
||||||
|
_ => std::option::Option::None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => std::option::Option::None,
|
||||||
|
};
|
||||||
|
let observed = observed.map(|value| return value.key.clone());
|
||||||
|
let valid = expected.is_some() && expected == observed;
|
||||||
|
return serde_json::json!({"expected":expected,"observed":observed,"valid":valid});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn token_kind(token: std::option::Option<&str>) -> serde_json::Value {
|
||||||
|
return match token {
|
||||||
|
std::option::Option::Some(kb_program_ids::SPL_TOKEN_PROGRAM_ID) => {
|
||||||
|
serde_json::json!({"programId":kb_program_ids::SPL_TOKEN_PROGRAM_ID,"kind":"spl_token_classic","supported":true})
|
||||||
|
},
|
||||||
|
std::option::Option::Some(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID) => {
|
||||||
|
serde_json::json!({"programId":kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,"kind":"token_2022","supported":true})
|
||||||
|
},
|
||||||
|
std::option::Option::Some(value) => {
|
||||||
|
serde_json::json!({"programId":value,"kind":"unsupported","supported":false})
|
||||||
|
},
|
||||||
|
std::option::Option::None => {
|
||||||
|
serde_json::json!({"programId":serde_json::Value::Null,"kind":"missing","supported":false})
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn failed(
|
||||||
|
entry: std::option::Option<&str>,
|
||||||
|
code: &str,
|
||||||
|
message: std::string::String,
|
||||||
|
) -> crate::DecoderExecutionResult {
|
||||||
|
return crate::DecoderExecutionResult {
|
||||||
|
status: crate::DecoderOutcomeStatus::Failed,
|
||||||
|
recognized_entry_code: entry.map(|value| return value.to_string()),
|
||||||
|
observations: std::vec::Vec::new(),
|
||||||
|
diagnostics: std::vec![crate::DecoderDiagnostic {
|
||||||
|
code: code.to_string(),
|
||||||
|
message,
|
||||||
|
retriable: false
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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(bytes: &[u8]) -> std::string::String {
|
||||||
|
return bytes.iter().take(16).map(|value| return format!("{value:02x}")).collect();
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-lib/src/lib.rs
|
// file: kb-lib/src/lib.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Consolidated decoder, executor, materializer and shared model library.
|
//! Consolidated decoder, executor, materializer and shared model library.
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
@@ -11,6 +11,24 @@ pub mod executor;
|
|||||||
pub mod materializer;
|
pub mod materializer;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
|
|
||||||
|
/// Current stable Associated Token Account decoded event contract version.
|
||||||
|
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_EVENT_VERSION;
|
||||||
|
/// Exact Associated Token Account instructions published by the official interface.
|
||||||
|
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_INSTRUCTION_ENTRIES;
|
||||||
|
/// Maximum retained account-key text length for the Associated Token Account decoder.
|
||||||
|
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNT_KEY_BYTES;
|
||||||
|
/// Maximum retained instruction account count for the Associated Token Account decoder.
|
||||||
|
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_ACCOUNTS;
|
||||||
|
/// Maximum retained semantic diagnostic count for the Associated Token Account decoder.
|
||||||
|
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_DIAGNOSTICS;
|
||||||
|
/// Maximum retained Associated Token Account instruction payload size.
|
||||||
|
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_INSTRUCTION_BYTES;
|
||||||
|
/// Maximum retained transaction account-key count for the Associated Token Account decoder.
|
||||||
|
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_MAX_TRANSACTION_KEYS;
|
||||||
|
/// Stable protocol and surface code for the Associated Token Account program.
|
||||||
|
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_SURFACE_CODE;
|
||||||
|
/// Canonical tracing target for the Associated Token Account decoder.
|
||||||
|
pub(crate) use crate::decoder::SPL_ASSOCIATED_TOKEN_ACCOUNT_TRACING_TARGET;
|
||||||
/// Maximum payload prefix retained for bounded Memo diagnostics.
|
/// Maximum payload prefix retained for bounded Memo diagnostics.
|
||||||
pub(crate) use crate::decoder::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
|
pub(crate) use crate::decoder::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
|
||||||
/// Current stable Memo decoded event contract version.
|
/// Current stable Memo decoded event contract version.
|
||||||
@@ -93,6 +111,12 @@ pub(crate) use crate::decoder::solana::solana_core_config_decode;
|
|||||||
pub(crate) use crate::decoder::solana::solana_core_config_recognize;
|
pub(crate) use crate::decoder::solana::solana_core_config_recognize;
|
||||||
/// Resolves positional instruction accounts and validates their core indexes.
|
/// Resolves positional instruction accounts and validates their core indexes.
|
||||||
pub(crate) use crate::decoder::solana::solana_core_resolve_accounts;
|
pub(crate) use crate::decoder::solana::solana_core_resolve_accounts;
|
||||||
|
/// Decodes one bounded Associated Token Account instruction.
|
||||||
|
pub(crate) use crate::decoder::spl_associated_token_account_decode;
|
||||||
|
/// Returns the exact entry matching an Associated Token Account wire.
|
||||||
|
pub(crate) use crate::decoder::spl_associated_token_account_entry;
|
||||||
|
/// Reads one bounded Associated Token Account payload.
|
||||||
|
pub(crate) use crate::decoder::spl_associated_token_account_payload;
|
||||||
/// Decodes one bounded SPL Memo instruction.
|
/// Decodes one bounded SPL Memo instruction.
|
||||||
pub(crate) use crate::decoder::spl_memo_decode;
|
pub(crate) use crate::decoder::spl_memo_decode;
|
||||||
/// Decodes one bounded classic SPL Token instruction.
|
/// Decodes one bounded classic SPL Token instruction.
|
||||||
@@ -224,6 +248,8 @@ pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_decode;
|
|||||||
/// Recognizes one historical ZK Token Proof layout or the current no-op runtime fallback.
|
/// Recognizes one historical ZK Token Proof layout or the current no-op runtime fallback.
|
||||||
pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_recognize;
|
pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_recognize;
|
||||||
|
|
||||||
|
/// Exact decoder for the SPL Associated Token Account program.
|
||||||
|
pub use crate::decoder::SplAssociatedTokenAccountDecoder;
|
||||||
/// Exact decoder for the three registered SPL Memo generations.
|
/// Exact decoder for the three registered SPL Memo generations.
|
||||||
pub use crate::decoder::SplMemoDecoder;
|
pub use crate::decoder::SplMemoDecoder;
|
||||||
/// Exact decoder for the classic SPL Token program.
|
/// Exact decoder for the classic SPL Token program.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<!-- file: kb-program-ids/README.md -->
|
<!-- file: kb-program-ids/README.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# kb-program-ids
|
# 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.
|
`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. Les identifiants SPL et protocolaires supplémentaires seront ajoutés avec leurs tranches de migration respectives.
|
La tranche `0.1.0-pre.004` expose les 18 surfaces exécutables nécessaires au décodeur Solana Core, les comptes natifs associés utilisés par leurs contrats et l’identifiant SPL Token classique employé par les tests de frontière. La tranche `0.1.0-pre.005` ajoute les trois identifiants SPL Memo v1, v3 et v4 sans les classer parmi les surfaces natives. `0.1.0-pre.006` active l’identifiant SPL Token classique dans un décodeur concret tout en le maintenant hors des surfaces natives. `0.1.0-pre.007` ajoute les identifiants SPL Associated Token Account et Token‑2022 nécessaires à la frontière ATA, toujours sans les classer parmi les surfaces natives.
|
||||||
|
|
||||||
## API publique
|
## API publique
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// file: kb-program-ids/src/constants.rs
|
// file: kb-program-ids/src/constants.rs
|
||||||
// version: 2
|
// version: 3
|
||||||
|
|
||||||
//! Canonical Solana program and well-known account identifiers.
|
//! Canonical Solana program and well-known account identifiers.
|
||||||
|
|
||||||
|
/// SPL Associated Token Account program identifier.
|
||||||
|
pub const ASSOCIATED_TOKEN_PROGRAM_ID: &str = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
||||||
/// Address Lookup Table program identifier.
|
/// Address Lookup Table program identifier.
|
||||||
pub const ADDRESS_LOOKUP_TABLE_PROGRAM_ID: &str = "AddressLookupTab1e1111111111111111111111111";
|
pub const ADDRESS_LOOKUP_TABLE_PROGRAM_ID: &str = "AddressLookupTab1e1111111111111111111111111";
|
||||||
/// Deprecated BPF Loader program identifier.
|
/// Deprecated BPF Loader program identifier.
|
||||||
@@ -39,6 +41,8 @@ pub const SPL_MEMO_V3_PROGRAM_ID: &str = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLG
|
|||||||
pub const SPL_MEMO_V4_PROGRAM_ID: &str = "Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH";
|
pub const SPL_MEMO_V4_PROGRAM_ID: &str = "Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH";
|
||||||
/// Classic SPL Token program identifier.
|
/// Classic SPL Token program identifier.
|
||||||
pub const SPL_TOKEN_PROGRAM_ID: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
pub const SPL_TOKEN_PROGRAM_ID: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
||||||
|
/// SPL Token-2022 program identifier.
|
||||||
|
pub const SPL_TOKEN_2022_PROGRAM_ID: &str = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb";
|
||||||
/// Historical Stake configuration account identifier.
|
/// Historical Stake configuration account identifier.
|
||||||
pub const STAKE_CONFIG_ACCOUNT_ID: &str = "StakeConfig11111111111111111111111111111111";
|
pub const STAKE_CONFIG_ACCOUNT_ID: &str = "StakeConfig11111111111111111111111111111111";
|
||||||
/// Compatibility alias for the historical Stake configuration account identifier.
|
/// Compatibility alias for the historical Stake configuration account identifier.
|
||||||
@@ -151,6 +155,10 @@ pub(crate) const NATIVE_WELL_KNOWN_ACCOUNT_ID_ENTRIES: &[crate::ProgramIdEntry]
|
|||||||
];
|
];
|
||||||
|
|
||||||
pub(crate) const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
|
pub(crate) const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
|
||||||
|
crate::ProgramIdEntry {
|
||||||
|
name: "associated_token_account",
|
||||||
|
address: crate::ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
|
},
|
||||||
crate::ProgramIdEntry {
|
crate::ProgramIdEntry {
|
||||||
name: "address_lookup_table",
|
name: "address_lookup_table",
|
||||||
address: crate::ADDRESS_LOOKUP_TABLE_PROGRAM_ID,
|
address: crate::ADDRESS_LOOKUP_TABLE_PROGRAM_ID,
|
||||||
@@ -223,6 +231,10 @@ pub(crate) const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
|
|||||||
name: "spl_token",
|
name: "spl_token",
|
||||||
address: crate::SPL_TOKEN_PROGRAM_ID,
|
address: crate::SPL_TOKEN_PROGRAM_ID,
|
||||||
},
|
},
|
||||||
|
crate::ProgramIdEntry {
|
||||||
|
name: "spl_token_2022",
|
||||||
|
address: crate::SPL_TOKEN_2022_PROGRAM_ID,
|
||||||
|
},
|
||||||
crate::ProgramIdEntry {
|
crate::ProgramIdEntry {
|
||||||
name: "stake_config",
|
name: "stake_config",
|
||||||
address: crate::STAKE_CONFIG_ACCOUNT_ID,
|
address: crate::STAKE_CONFIG_ACCOUNT_ID,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-program-ids/src/lib.rs
|
// file: kb-program-ids/src/lib.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
@@ -11,6 +11,8 @@ mod constants;
|
|||||||
|
|
||||||
/// Address Lookup Table program identifier.
|
/// Address Lookup Table program identifier.
|
||||||
pub use constants::ADDRESS_LOOKUP_TABLE_PROGRAM_ID;
|
pub use constants::ADDRESS_LOOKUP_TABLE_PROGRAM_ID;
|
||||||
|
/// SPL Associated Token Account program identifier.
|
||||||
|
pub use constants::ASSOCIATED_TOKEN_PROGRAM_ID;
|
||||||
/// Deprecated BPF Loader program identifier.
|
/// Deprecated BPF Loader program identifier.
|
||||||
pub use constants::BPF_LOADER_DEPRECATED_PROGRAM_ID;
|
pub use constants::BPF_LOADER_DEPRECATED_PROGRAM_ID;
|
||||||
/// BPF Loader version 2 program identifier.
|
/// BPF Loader version 2 program identifier.
|
||||||
@@ -49,6 +51,8 @@ pub use constants::SPL_MEMO_V1_PROGRAM_ID;
|
|||||||
pub use constants::SPL_MEMO_V3_PROGRAM_ID;
|
pub use constants::SPL_MEMO_V3_PROGRAM_ID;
|
||||||
/// Current SPL Memo v4 program identifier.
|
/// Current SPL Memo v4 program identifier.
|
||||||
pub use constants::SPL_MEMO_V4_PROGRAM_ID;
|
pub use constants::SPL_MEMO_V4_PROGRAM_ID;
|
||||||
|
/// SPL Token-2022 program identifier.
|
||||||
|
pub use constants::SPL_TOKEN_2022_PROGRAM_ID;
|
||||||
/// Classic SPL Token program identifier.
|
/// Classic SPL Token program identifier.
|
||||||
pub use constants::SPL_TOKEN_PROGRAM_ID;
|
pub use constants::SPL_TOKEN_PROGRAM_ID;
|
||||||
/// Historical Stake configuration account identifier.
|
/// Historical Stake configuration account identifier.
|
||||||
@@ -138,7 +142,7 @@ mod tests {
|
|||||||
assert!(codes.insert(entry.code()));
|
assert!(codes.insert(entry.code()));
|
||||||
assert!(program_ids.insert(entry.program_id()));
|
assert!(program_ids.insert(entry.program_id()));
|
||||||
}
|
}
|
||||||
assert_eq!(crate::registered_program_ids().len(), 29);
|
assert_eq!(crate::registered_program_ids().len(), 31);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -201,6 +205,8 @@ mod tests {
|
|||||||
crate::SPL_MEMO_V3_PROGRAM_ID,
|
crate::SPL_MEMO_V3_PROGRAM_ID,
|
||||||
crate::SPL_MEMO_V4_PROGRAM_ID,
|
crate::SPL_MEMO_V4_PROGRAM_ID,
|
||||||
crate::SPL_TOKEN_PROGRAM_ID,
|
crate::SPL_TOKEN_PROGRAM_ID,
|
||||||
|
crate::SPL_TOKEN_2022_PROGRAM_ID,
|
||||||
|
crate::ASSOCIATED_TOKEN_PROGRAM_ID,
|
||||||
];
|
];
|
||||||
for program_id in spl_program_ids {
|
for program_id in spl_program_ids {
|
||||||
assert!(crate::find_registered_program_id(program_id).is_some());
|
assert!(crate::find_registered_program_id(program_id).is_some());
|
||||||
|
|||||||
Reference in New Issue
Block a user