This commit is contained in:
2026-07-23 16:37:12 +02:00
parent 99c345f2f2
commit 0da75c1311
2159 changed files with 230833 additions and 0 deletions

View File

@@ -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

View File

@@ -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);
```

View File

@@ -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";

View File

@@ -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"
);
}
}

View File

@@ -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