0.1.0
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
# file: kb_decoder_spl_token/Cargo.toml
|
||||
# version: 3
|
||||
|
||||
[package]
|
||||
name = "kb_decoder_spl_token"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64.workspace = true
|
||||
bs58.workspace = true
|
||||
kb_core = { path = "../kb_core" }
|
||||
kb_decoder_api = { path = "../kb_decoder_api" }
|
||||
kb_model = { path = "../kb_model" }
|
||||
kb_program_ids = { path = "../kb_program_ids" }
|
||||
kb_store_core = { path = "../kb_store_core" }
|
||||
serde_json.workspace = true
|
||||
sha2.workspace = true
|
||||
spl-token-interface.workspace = true
|
||||
tracing.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,89 @@
|
||||
<!-- file: kb_decoder_spl_token/README.md -->
|
||||
<!-- version: 4 -->
|
||||
|
||||
# kb_decoder_spl_token
|
||||
|
||||
Ce crate décode exclusivement le programme SPL Token classique
|
||||
`TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`. Token-2022, ATA, les interfaces
|
||||
de métadonnées et les programmes qui invoquent Token par CPI ne sont pas absorbés dans sa
|
||||
surface. Une instruction Token inner reste néanmoins décodée grâce à son Program ID exact et à
|
||||
son chemin core stable.
|
||||
|
||||
## Surface couverte
|
||||
|
||||
La couverture compilée contient les 28 tags publiés par `spl-token-interface 3.0.0` : `0..24`,
|
||||
`38`, `45` et `255`. Elle inclut les variantes historiques, checked, les deux générations sans
|
||||
Rent, les conversions montant/UI, `WithdrawExcessLamports`, `UnwrapLamports` et `Batch`.
|
||||
|
||||
La publication d'un tag et son déploiement sont traités séparément. `UnwrapLamports` et `Batch`
|
||||
sont constructibles dans l'interface `3.0.0` et implémentés par p-token `1.0.0`. Leur déploiement
|
||||
sur le programme classique Devnet est observé par deux simulations réussies le 16 juillet 2026 ;
|
||||
Localnet et Mainnet restent non testés pour ces tags récents. Les opérations classiques observées
|
||||
sur Mainnet ou exécutées sur Devnet portent séparément leurs preuves réelles. La matrice normative
|
||||
est `docs/SPL_TOKEN_MATRIX.json`.
|
||||
|
||||
## Contrat de décodage
|
||||
|
||||
`SplTokenDecoder` implémente les deux contrats communs :
|
||||
|
||||
- `ProtocolDecoder` annonce `Yes` uniquement pour le Program ID Token classique exact ;
|
||||
- `InstructionDecoder` fournit une surface, 28 déclarations de couverture, une reconnaissance par
|
||||
tag et un résultat contextualisé pour le replay commun.
|
||||
|
||||
Chaque observation conserve le tag, le wire borné et son SHA-256, le chemin outer/inner, les
|
||||
comptes dans leur ordre d'origine avec doublons et flags, les rôles prouvables, les paramètres bruts,
|
||||
la forme d'autorité structurelle et les diagnostics. Les montants `u64` sont écrits en chaînes
|
||||
décimales dans le JSON pour éviter toute perte à la frontière Tauri.
|
||||
|
||||
Un `Transfer` non checked ne reçoit jamais de mint ou de decimals inventés. Le champ d'inférence
|
||||
reste explicitement vide tant qu'aucune corrélation core prouvée n'est implémentée. Le décodeur
|
||||
n'effectue aucune lecture RPC.
|
||||
|
||||
## Transactions échouées et autorités
|
||||
|
||||
Une transaction échouée mais lisible produit une intention structurée avec `committed=false`. Elle
|
||||
ne prouve aucune mutation de compte Token. Les formes simple et multisig sont distinguées depuis
|
||||
les metas fournies ; la validation de `M/N` et des membres d'un multisig existant exige un snapshot
|
||||
stateful et n'est jamais inventée par le décodeur.
|
||||
|
||||
Pour `InitializeMultisig` et `InitializeMultisig2`, le wire `M`, le nombre `N` déduit des comptes et
|
||||
les bornes officielles `1 <= M <= N <= 11` sont contrôlés structurellement.
|
||||
|
||||
## Batch
|
||||
|
||||
`Batch` est analysé avec des bornes locales de 64 sous-instructions, 512 comptes cumulés et
|
||||
16 384 octets de payload. Chaque entrée conserve son slice de comptes, son offset de données, son
|
||||
ordre et un chemin dérivé `<parent>/batch/<position>`. Un batch vide, tronqué, avec longueur nulle,
|
||||
comptes insuffisants ou batch imbriqué échoue avec un diagnostic borné.
|
||||
|
||||
## Limites actuelles
|
||||
|
||||
- Le contrat `CoreInstructionReplayInput` ne transporte pas encore les return data canoniques ; les
|
||||
trois conversions concernées restent décodées comme intentions, avec validation de return data
|
||||
explicitement indisponible.
|
||||
- Le corpus Mainnet prouve les opérations réellement observées, pas l'exhaustivité des 28 tags sur
|
||||
ce cluster. Les initialisations normalisées par la matérialisation ne sont pas attribuées à une
|
||||
variante wire sans preuve supplémentaire.
|
||||
- Localnet et Mainnet restent sans preuve de déploiement de `UnwrapLamports` et `Batch` ; la preuve
|
||||
Devnet n'est pas extrapolée aux autres clusters.
|
||||
|
||||
## Corpus réel
|
||||
|
||||
Le replay Mainnet du 15 juillet 2026 a décodé 370 instructions sans entrée unsupported, failed ou
|
||||
unmatched. Il a produit 371 sorties commitées, huit refus correspondant exactement aux huit
|
||||
transactions échouées et un second replay avec zéro sélection. Les signatures représentatives de
|
||||
`Transfer`, `Approve`, `CloseAccount`, `TransferChecked`, `BurnChecked` et `SyncNative` sont
|
||||
conservées par instruction dans la matrice. Le lifecycle Devnet y conserve séparément les
|
||||
signatures exactes de ses opérations courantes.
|
||||
|
||||
Le probe Devnet du 16 juillet 2026 a simulé sans signature ni envoi un `Batch` contenant un
|
||||
`TransferChecked` de montant nul, puis un `UnwrapLamports` d'un lamport depuis un compte wrapped SOL
|
||||
auxiliaire classique. Les deux simulations ont réussi sans erreur runtime, avec respectivement 270
|
||||
et 140 compute units.
|
||||
|
||||
## Exemple
|
||||
|
||||
```rust
|
||||
let decoder = kb_decoder_spl_token::SplTokenDecoder;
|
||||
let result = kb_decoder_api::InstructionDecoder::decode(&decoder, &input);
|
||||
```
|
||||
@@ -0,0 +1,50 @@
|
||||
// file: kb_decoder_spl_token/src/constants.rs
|
||||
// version: 3
|
||||
|
||||
//! Local constants for the `kb_decoder_spl_token` crate. Program identifiers live in `kb_program_ids`.
|
||||
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) const SURFACE_CODE: &str = "spl_token";
|
||||
/// Current stable decoded event contract version.
|
||||
pub(crate) const TOKEN_EVENT_VERSION: u32 = 1;
|
||||
/// Maximum retained instruction payload size.
|
||||
pub(crate) const MAX_INSTRUCTION_BYTES: usize = 16_384;
|
||||
/// Maximum prefix retained in diagnostics.
|
||||
pub(crate) const DIAGNOSTIC_PREFIX_BYTES: usize = 32;
|
||||
/// Maximum decoded sub-instructions in one batch.
|
||||
pub(crate) const MAX_BATCH_INSTRUCTIONS: usize = 64;
|
||||
/// Maximum total account metas consumed by one batch.
|
||||
pub(crate) const MAX_BATCH_ACCOUNTS: usize = 512;
|
||||
/// One exact instruction entry published by `spl-token-interface` 3.0.0.
|
||||
pub(crate) const INSTRUCTION_ENTRIES: &[(u8, &str, bool)] = &[
|
||||
(0, "initialize_mint", true),
|
||||
(1, "initialize_account", true),
|
||||
(2, "initialize_multisig", true),
|
||||
(3, "transfer", true),
|
||||
(4, "approve", true),
|
||||
(5, "revoke", true),
|
||||
(6, "set_authority", true),
|
||||
(7, "mint_to", true),
|
||||
(8, "burn", true),
|
||||
(9, "close_account", true),
|
||||
(10, "freeze_account", true),
|
||||
(11, "thaw_account", true),
|
||||
(12, "transfer_checked", false),
|
||||
(13, "approve_checked", false),
|
||||
(14, "mint_to_checked", false),
|
||||
(15, "burn_checked", false),
|
||||
(16, "initialize_account2", true),
|
||||
(17, "sync_native", false),
|
||||
(18, "initialize_account3", false),
|
||||
(19, "initialize_multisig2", false),
|
||||
(20, "initialize_mint2", false),
|
||||
(21, "get_account_data_size", false),
|
||||
(22, "initialize_immutable_owner", false),
|
||||
(23, "amount_to_ui_amount", false),
|
||||
(24, "ui_amount_to_amount", false),
|
||||
(38, "withdraw_excess_lamports", false),
|
||||
(45, "unwrap_lamports", false),
|
||||
(255, "batch", false),
|
||||
];
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb_decoder_spl_token";
|
||||
@@ -0,0 +1,512 @@
|
||||
// file: kb_decoder_spl_token/src/decoder.rs
|
||||
// version: 12
|
||||
|
||||
//! Exact classic SPL Token dispatch for the common decode pipeline.
|
||||
|
||||
const TOKEN_SURFACES: &[kb_decoder_api::DecoderSurface] = &[kb_decoder_api::DecoderSurface {
|
||||
program_id: kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
surface_code: crate::SURFACE_CODE,
|
||||
priority: 100,
|
||||
}];
|
||||
|
||||
const PROGRAM_IDS: &[&str] = &[kb_program_ids::SPL_TOKEN_PROGRAM_ID];
|
||||
|
||||
/// Exact decoder for the classic SPL Token program.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SplTokenDecoder;
|
||||
|
||||
impl kb_decoder_api::ProtocolDecoder for crate::SplTokenDecoder {
|
||||
fn decoder_name(&self) -> &'static str {
|
||||
return "kb_decoder_spl_token";
|
||||
}
|
||||
|
||||
fn decoder_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn program_ids(&self) -> &'static [&'static str] {
|
||||
return PROGRAM_IDS;
|
||||
}
|
||||
|
||||
fn supports_observation(
|
||||
&self,
|
||||
observation: &kb_model::ProgramObservation,
|
||||
) -> kb_decoder_api::DecoderSupport {
|
||||
return if kb_decoder_api::ProtocolDecoder::handles_program_id(self, &observation.program_id)
|
||||
{
|
||||
kb_decoder_api::DecoderSupport::Yes
|
||||
} else {
|
||||
kb_decoder_api::DecoderSupport::No
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_observation(
|
||||
&self,
|
||||
_observation: &kb_model::ProgramObservation,
|
||||
) -> kb_core::Result<std::vec::Vec<kb_model::DecodedProtocolEvent>> {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_decoder_api::InstructionDecoder for crate::SplTokenDecoder {
|
||||
fn identity(&self) -> kb_decoder_api::DecoderIdentity {
|
||||
return kb_decoder_api::DecoderIdentity {
|
||||
name: "spl_token".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn surfaces(&self) -> &'static [kb_decoder_api::DecoderSurface] {
|
||||
return TOKEN_SURFACES;
|
||||
}
|
||||
|
||||
fn coverage(&self) -> std::vec::Vec<kb_decoder_api::DecoderCoverageDeclaration> {
|
||||
return crate::INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.map(|(tag, code, historical)| {
|
||||
return kb_decoder_api::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string(),
|
||||
surface_code: std::option::Option::Some(crate::SURFACE_CODE.to_string()),
|
||||
entry_kind: kb_decoder_api::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: (*code).to_string(),
|
||||
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
|
||||
historical: *historical,
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn recognize(
|
||||
&self,
|
||||
input: &kb_store_core::CoreInstructionReplayInput,
|
||||
) -> kb_decoder_api::DecoderRecognition {
|
||||
if input.program_id != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
||||
return kb_decoder_api::DecoderRecognition::incompatible();
|
||||
}
|
||||
let tag = crate::payload_tag(input);
|
||||
let entry = tag.and_then(crate::entry_for_tag);
|
||||
return kb_decoder_api::DecoderRecognition::compatible(
|
||||
entry.is_some(),
|
||||
100,
|
||||
std::option::Option::Some(crate::SURFACE_CODE.to_string()),
|
||||
entry.map(|(_, code, _)| return code.to_string()),
|
||||
tag.map(|value| return format!("{value:02x}")),
|
||||
);
|
||||
}
|
||||
|
||||
fn decode(
|
||||
&self,
|
||||
input: &kb_store_core::CoreInstructionReplayInput,
|
||||
) -> kb_decoder_api::DecoderExecutionResult {
|
||||
if input.program_id != kb_program_ids::SPL_TOKEN_PROGRAM_ID {
|
||||
return kb_decoder_api::DecoderExecutionResult::unsupported(std::option::Option::None);
|
||||
}
|
||||
let result = crate::decode(input);
|
||||
if matches!(
|
||||
result.status,
|
||||
kb_decoder_api::DecoderOutcomeStatus::Failed
|
||||
| kb_decoder_api::DecoderOutcomeStatus::Unsupported
|
||||
) {
|
||||
tracing::error!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "decode_failure",
|
||||
signature = %input.signature,
|
||||
slot = input.slot,
|
||||
instruction_path = %input.instruction_path,
|
||||
program_id = %input.program_id,
|
||||
processor_name = "spl_token",
|
||||
processor_version = env!("CARGO_PKG_VERSION"),
|
||||
input_key = %input.replay_input_key,
|
||||
payload_hash = ?input.instruction_payload_hash,
|
||||
result_status = ?result.status,
|
||||
diagnostics = ?result.diagnostics,
|
||||
"classic SPL Token instruction was not decoded successfully"
|
||||
);
|
||||
}
|
||||
tracing::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "decode",
|
||||
signature = %input.signature,
|
||||
instruction_path = %input.instruction_path,
|
||||
transaction_failed = input.transaction_failed,
|
||||
result_status = ?result.status,
|
||||
observation_count = result.observations.len(),
|
||||
"classic SPL Token instruction decode completed"
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn input(
|
||||
program_id: &str,
|
||||
payload: &[u8],
|
||||
accounts: serde_json::Value,
|
||||
keys: serde_json::Value,
|
||||
failed: bool,
|
||||
path: &str,
|
||||
) -> kb_store_core::CoreInstructionReplayInput {
|
||||
let result = kb_store_core::CoreInstructionReplayInput::new(
|
||||
format!("signature:{path}"),
|
||||
"signature",
|
||||
42,
|
||||
path,
|
||||
program_id,
|
||||
failed,
|
||||
if failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
keys,
|
||||
accounts,
|
||||
std::option::Option::Some(serde_json::json!({
|
||||
"dataBase64": base64::engine::general_purpose::STANDARD.encode(payload)
|
||||
})),
|
||||
std::option::Option::None,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("Token replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
fn minimal_wire(tag: u8) -> std::vec::Vec<u8> {
|
||||
return match tag {
|
||||
0 | 20 => {
|
||||
let mut value = std::vec![tag, 9];
|
||||
value.extend_from_slice(&[1; 32]);
|
||||
value.push(0);
|
||||
value
|
||||
},
|
||||
2 | 19 => std::vec![tag, 1],
|
||||
3 | 4 | 7 | 8 | 23 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&1_u64.to_le_bytes());
|
||||
value
|
||||
},
|
||||
6 => std::vec![tag, 0, 0],
|
||||
12 | 13 | 14 | 15 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&1_u64.to_le_bytes());
|
||||
value.push(9);
|
||||
value
|
||||
},
|
||||
16 | 18 => {
|
||||
let mut value = std::vec![tag];
|
||||
value.extend_from_slice(&[2; 32]);
|
||||
value
|
||||
},
|
||||
24 => std::vec![tag, b'1'],
|
||||
45 => std::vec![tag, 0],
|
||||
255 => std::vec![tag, 0, 1, 1],
|
||||
_ => std::vec![tag],
|
||||
};
|
||||
}
|
||||
|
||||
fn account(position: usize, signer: bool, writable: bool) -> serde_json::Value {
|
||||
return serde_json::json!({
|
||||
"position": position,
|
||||
"accountIndex": position,
|
||||
"accountKey": format!("account{position}"),
|
||||
"signer": signer,
|
||||
"writable": writable,
|
||||
"source": "static",
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_support_program_id_and_interface_match() {
|
||||
assert_eq!(spl_token_interface::ID.to_string(), kb_program_ids::SPL_TOKEN_PROGRAM_ID);
|
||||
let decoder = crate::SplTokenDecoder;
|
||||
assert_eq!(kb_decoder_api::InstructionDecoder::surfaces(&decoder).len(), 1);
|
||||
let observation = kb_model::ProgramObservation {
|
||||
signature: kb_model::Signature("signature".to_string()),
|
||||
slot: kb_model::Slot(1),
|
||||
instruction_path: kb_model::InstructionPath("0".to_string()),
|
||||
program_id: kb_model::ProgramId(kb_program_ids::SPL_TOKEN_PROGRAM_ID.to_string()),
|
||||
discriminator_8: std::option::Option::None,
|
||||
data_len: 1,
|
||||
accounts_len: 0,
|
||||
failed: false,
|
||||
};
|
||||
assert_eq!(
|
||||
kb_decoder_api::ProtocolDecoder::supports_observation(&decoder, &observation),
|
||||
kb_decoder_api::DecoderSupport::Yes
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coverage_matrix_and_compiled_entries_are_equal() {
|
||||
let matrix: serde_json::Value =
|
||||
match serde_json::from_str(include_str!("../../docs/SPL_TOKEN_MATRIX.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("SPL Token matrix is invalid: {error}"),
|
||||
};
|
||||
let rows = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("SPL Token matrix has no instructions"),
|
||||
};
|
||||
let matrix_entries = rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
return (
|
||||
row.get("tag").and_then(serde_json::Value::as_u64).unwrap_or(u64::MAX) as u8,
|
||||
row.get("name").and_then(serde_json::Value::as_str).unwrap_or(""),
|
||||
);
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let compiled_entries = crate::INSTRUCTION_ENTRIES
|
||||
.iter()
|
||||
.map(|(tag, code, _)| return (*tag, *code))
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
assert_eq!(matrix_entries, compiled_entries);
|
||||
assert_eq!(kb_decoder_api::InstructionDecoder::coverage(&crate::SplTokenDecoder).len(), 28);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cluster_evidence_scopes_recent_instruction_deployment_exactly() {
|
||||
let matrix: serde_json::Value =
|
||||
match serde_json::from_str(include_str!("../../docs/SPL_TOKEN_MATRIX.json")) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("SPL Token matrix is invalid: {error}"),
|
||||
};
|
||||
assert_eq!(
|
||||
matrix["deploymentAudit"]["mainnet"]["status"],
|
||||
"real_corpus_decode_and_materialization_validated"
|
||||
);
|
||||
assert_eq!(matrix["corpusAudit"]["mainnet"]["invariants"]["uncommittedOutputs"], 0);
|
||||
assert_eq!(matrix["corpusAudit"]["mainnet"]["invariants"]["secondReplaySelected"], 0);
|
||||
assert_eq!(matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["submission"], false);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["batch"]["success"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["batch"]["computeUnits"],
|
||||
270
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["unwrapLamports"]["success"],
|
||||
true
|
||||
);
|
||||
assert_eq!(
|
||||
matrix["corpusAudit"]["devnet"]["recentInstructionProbe"]["unwrapLamports"]["computeUnits"],
|
||||
140
|
||||
);
|
||||
|
||||
let instructions = match matrix.get("instructions").and_then(serde_json::Value::as_array) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => panic!("SPL Token matrix has no instructions"),
|
||||
};
|
||||
for recent_name in ["unwrap_lamports", "batch"] {
|
||||
let recent = match instructions.iter().find(|row| {
|
||||
return row.get("name").and_then(serde_json::Value::as_str)
|
||||
== std::option::Option::Some(recent_name);
|
||||
}) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
panic!("recent SPL Token instruction {recent_name} is absent")
|
||||
},
|
||||
};
|
||||
assert_eq!(recent["executorSupport"]["constructibility"], "official_builder_proven");
|
||||
assert_eq!(
|
||||
recent["executorSupport"]["clusterDeployment"],
|
||||
"devnet_simulation_succeeded_2026-07-16"
|
||||
);
|
||||
assert_eq!(recent["clusterStatus"]["localnet"], "not_tested");
|
||||
assert_eq!(recent["clusterStatus"]["devnet"], "simulation_succeeded_2026-07-16");
|
||||
assert_eq!(recent["clusterStatus"]["mainnet"], "not_tested");
|
||||
assert_eq!(recent["realSignatures"].as_array().map(std::vec::Vec::len), Some(0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_published_tag_decodes_and_matches_official_unpack() {
|
||||
for (tag, code, _) in crate::INSTRUCTION_ENTRIES {
|
||||
let wire = minimal_wire(*tag);
|
||||
let official = spl_token_interface::instruction::TokenInstruction::unpack(&wire);
|
||||
assert!(official.is_ok(), "official unpack rejected tag {tag}");
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result =
|
||||
kb_decoder_api::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some(*code));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_transfer_is_an_exact_uncommitted_inner_intent_without_invented_mint() {
|
||||
let mut wire = std::vec![3];
|
||||
wire.extend_from_slice(&u64::MAX.to_le_bytes());
|
||||
let accounts = serde_json::json!([
|
||||
{"position":0,"accountIndex":0,"accountKey":"source"},
|
||||
{"position":1,"accountIndex":1,"accountKey":"destination"},
|
||||
{"position":2,"accountIndex":2,"accountKey":"authority"}
|
||||
]);
|
||||
let keys = serde_json::json!([
|
||||
{"accountIndex":0,"accountKey":"source","signer":false,"writable":true,"source":"static"},
|
||||
{"accountIndex":1,"accountKey":"destination","signer":false,"writable":true,"source":"static"},
|
||||
{"accountIndex":2,"accountKey":"authority","signer":true,"writable":false,"source":"static"}
|
||||
]);
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
accounts,
|
||||
keys,
|
||||
true,
|
||||
"2/1",
|
||||
);
|
||||
let result = kb_decoder_api::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Decoded);
|
||||
assert!(!result.observations[0].observation_committed);
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["parameters"]["amountRaw"],
|
||||
u64::MAX.to_string()
|
||||
);
|
||||
assert_eq!(result.observations[0].payload_json["inference"]["mintInvented"], false);
|
||||
assert_eq!(
|
||||
result.observations[0].event.source_kind,
|
||||
kb_model::EventSourceKind::InnerInstruction
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suffix_unknown_empty_truncated_and_invalid_utf8_are_bounded() {
|
||||
let mut transfer = minimal_wire(3);
|
||||
transfer.extend_from_slice(&[0xaa, 0xbb]);
|
||||
let suffix = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
transfer.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let suffix_result =
|
||||
kb_decoder_api::InstructionDecoder::decode(&crate::SplTokenDecoder, &suffix);
|
||||
assert_eq!(suffix_result.status, kb_decoder_api::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(suffix_result.observations[0].payload_json["wire"]["suffixLengthBytes"], 2);
|
||||
for wire in [std::vec::Vec::new(), std::vec![3, 1], std::vec![24, 0xff], std::vec![255]] {
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result =
|
||||
kb_decoder_api::InstructionDecoder::decode(&crate::SplTokenDecoder, &input);
|
||||
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Failed);
|
||||
assert!(result.observations.is_empty());
|
||||
}
|
||||
let unknown = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
&[44],
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = kb_decoder_api::InstructionDecoder::decode(&crate::SplTokenDecoder, &unknown);
|
||||
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Unsupported);
|
||||
assert!(!result.diagnostics.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_preserves_order_slices_paths_and_rejects_nested_batch() {
|
||||
let mut batch = std::vec![255, 0, 9, 3];
|
||||
batch.extend_from_slice(&1_u64.to_le_bytes());
|
||||
batch.extend_from_slice(&[0, 1, 17]);
|
||||
let cinput = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
batch.as_slice(),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"4/2",
|
||||
);
|
||||
let result = kb_decoder_api::InstructionDecoder::decode(&crate::SplTokenDecoder, &cinput);
|
||||
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.observations.len(), 3);
|
||||
assert_eq!(result.observations[1].event.instruction_path.0, "4/2/batch/0");
|
||||
assert_eq!(result.observations[2].event.instruction_path.0, "4/2/batch/1");
|
||||
assert_eq!(result.observations[0].payload_json["parameters"]["subInstructionCount"], 2);
|
||||
let nested = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
&[255, 0, 1, 255],
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let nested_result =
|
||||
kb_decoder_api::InstructionDecoder::decode(&crate::SplTokenDecoder, &nested);
|
||||
assert_eq!(nested_result.status, kb_decoder_api::DecoderOutcomeStatus::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn authority_forms_and_multisig_threshold_diagnostics_are_explicit() {
|
||||
let wire = minimal_wire(3);
|
||||
let simple_accounts = serde_json::json!([
|
||||
{"position":0,"accountIndex":0,"accountKey":"account0"},
|
||||
{"position":1,"accountIndex":1,"accountKey":"account1"},
|
||||
{"position":2,"accountIndex":2,"accountKey":"account2"}
|
||||
]);
|
||||
let simple_keys = serde_json::json!([
|
||||
account(0, false, true),
|
||||
account(1, false, true),
|
||||
account(2, true, false)
|
||||
]);
|
||||
let simple = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
simple_accounts.clone(),
|
||||
simple_keys,
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let simple_result =
|
||||
kb_decoder_api::InstructionDecoder::decode(&crate::SplTokenDecoder, &simple);
|
||||
assert_eq!(simple_result.observations[0].payload_json["authority"]["form"], "single");
|
||||
let mut multisig_accounts = simple_accounts.as_array().cloned().unwrap_or_default();
|
||||
multisig_accounts
|
||||
.push(serde_json::json!({"position":3,"accountIndex":3,"accountKey":"account3"}));
|
||||
let multisig_keys = serde_json::json!([
|
||||
account(0, false, true),
|
||||
account(1, false, true),
|
||||
account(2, false, false),
|
||||
account(3, true, false)
|
||||
]);
|
||||
let multisig = input(
|
||||
kb_program_ids::SPL_TOKEN_PROGRAM_ID,
|
||||
wire.as_slice(),
|
||||
serde_json::Value::Array(multisig_accounts),
|
||||
multisig_keys,
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let multisig_result =
|
||||
kb_decoder_api::InstructionDecoder::decode(&crate::SplTokenDecoder, &multisig);
|
||||
assert_eq!(multisig_result.observations[0].payload_json["authority"]["form"], "multisig");
|
||||
assert_eq!(
|
||||
multisig_result.observations[0].payload_json["authority"]["statefulMultisigValidation"],
|
||||
"requires_multisig_account_snapshot"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// file: kb_decoder_spl_token/src/lib.rs
|
||||
// version: 5
|
||||
|
||||
//! Decoder crate for `spl_token`.
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod constants;
|
||||
mod decoder;
|
||||
mod token;
|
||||
|
||||
/// Maximum prefix retained in diagnostics.
|
||||
pub(crate) use crate::constants::DIAGNOSTIC_PREFIX_BYTES;
|
||||
/// One exact instruction entry published by `spl-token-interface` 3.0.0.
|
||||
pub(crate) use crate::constants::INSTRUCTION_ENTRIES;
|
||||
/// Maximum total account metas consumed by one batch.
|
||||
pub(crate) use crate::constants::MAX_BATCH_ACCOUNTS;
|
||||
/// Maximum decoded sub-instructions in one batch.
|
||||
pub(crate) use crate::constants::MAX_BATCH_INSTRUCTIONS;
|
||||
/// Maximum retained instruction payload size.
|
||||
pub(crate) use crate::constants::MAX_INSTRUCTION_BYTES;
|
||||
/// Stable protocol and surface code for the classic SPL Token program.
|
||||
pub(crate) use crate::constants::SURFACE_CODE;
|
||||
/// Current stable decoded event contract version.
|
||||
pub(crate) use crate::constants::TOKEN_EVENT_VERSION;
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) use crate::constants::TRACING_TARGET;
|
||||
/// Crate-root access to `decode` from `token`.
|
||||
pub(crate) use crate::token::decode;
|
||||
/// Crate-root access to `entry_for_tag` from `token`.
|
||||
pub(crate) use crate::token::entry_for_tag;
|
||||
/// Crate-root access to `payload_tag` from `token`.
|
||||
pub(crate) use crate::token::payload_tag;
|
||||
|
||||
/// Initial decoder implementation placeholder.
|
||||
pub use crate::decoder::SplTokenDecoder;
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user