0.1.0-pre.005

This commit is contained in:
2026-07-23 18:44:43 +02:00
parent 149d4c6ef6
commit 2a4479a1d4
17 changed files with 1033 additions and 53 deletions

View File

@@ -1,5 +1,5 @@
<!-- file: README.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# Khadhroony Bot3
@@ -50,3 +50,7 @@ cargo clippy --workspace --all-targets
- façade unique dans `kb-store/src/lib.rs` ;
- modèle de replay partagé conservé dans `kb-lib`, sans duplication ;
- options PostgreSQL indépendantes de `kb-config`, adaptées par la frontière applicative.
## 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, avec les mêmes contrats contextualisés, la même façade publique et sans dépendance inverse vers `kb-store`.

View File

@@ -1,5 +1,5 @@
<!-- file: ROADMAP.md -->
<!-- version: 2 -->
<!-- version: 3 -->
# ROADMAP — khadhroony-bot3
@@ -18,6 +18,7 @@
- [x] Fusionner `kb_store_core` et `kb_store_pg` dans `kb-store`.
- [ ] Adapter `kb-pipeline` aux nouveaux chemins publics.
- [ ] Adapter `kb-app-demo` et rétablir les validations fonctionnelles.
- [ ] Réorganiser les chemins dexport TSRS de `kb-lib` dans une tranche dédiée, en conservant `../frontend/ts/bindings/` comme préfixe de base.
- [ ] Auditer rétrospectivement la couverture historique des décodeurs et matérialisateurs.
### 0.1.0-pre.002 — Contrats fondamentaux consolidés
@@ -39,7 +40,7 @@
- [x] Adapter les règles bot2 devenues incompatibles avec larchitecture bot3.
- [x] Valider `cargo check`, les 84 tests et Clippy pour `kb-store`, puis `cargo check --workspace`.
- [ ] Valider `cargo test --workspace` et `cargo clippy --workspace --all-targets`.
- [ ] Valider les tests PostgreSQL réels avec `KB_POSTGRES_TEST_URL`.
- [x] Valider les 84 tests PostgreSQL réels avec `KB_POSTGRES_TEST_URL`.
### 0.1.0-pre.004 — Décodeur Solana Core dans `kb-lib`
@@ -48,5 +49,16 @@
- [x] Adapter les contrats vers les réexports de `kb-lib` sans dépendance vers `kb-store`.
- [x] Préfixer les symboles internes afin déviter les collisions avec les futurs décodeurs fusionnés.
- [x] Déclarer le target hiérarchique `kb-lib.decoder.solana.core`.
- [ ] Valider `kb-lib` et le workspace avec Cargo sur la machine de développement.
- [ ] Poursuivre avec SPL Memo après validation.
- [x] Valider `cargo check -p kb-lib`, les 169 tests de `kb-lib` et les deux audits Rust.
- [ ] Valider Clippy et lensemble du workspace sur la machine de développement.
- [x] Poursuivre avec SPL Memo après validation.
### 0.1.0-pre.005 — Décodeur SPL Memo dans `kb-lib`
- [x] Remplacer le scaffold `kb-lib::decoder::spl::memo` par le décodeur validé de bot2.
- [x] Conserver les générations v1, v3 et v4, leurs règles distinctes et les 9 tests.
- [x] Ajouter les trois Program IDs canoniques dans `kb-program-ids` sans les classer comme natifs.
- [x] Préserver le payload borné, le SHA-256, les comptes ordonnés et les statuts non commités.
- [x] Porter la matrice machine-readable `docs/SPL_MEMO_MATRIX.json`.
- [ ] Valider `kb-program-ids`, `kb-lib` et le workspace avec Cargo sur la machine de développement.
- [ ] Poursuivre avec SPL Token classique après validation.

View File

@@ -26,7 +26,7 @@
"kb-lib.decoder.solana.core",
"kb_decoder_spl_associated_token_account",
"kb_decoder_spl_elgamal_registry",
"kb_decoder_spl_memo",
"kb-lib.decoder.spl.memo",
"kb_decoder_spl_token",
"kb_decoder_spl_token_2022",
"kb_execution_solana",
@@ -186,42 +186,42 @@
]
},
{
"name": "file_kb_decoder_spl_memo_debug",
"name": "file_kb_lib_decoder_spl_memo_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/devnet/kb_decoder_spl_memo/debug.log",
"path": "logs/devnet/kb-lib/decoder/spl/memo/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_decoder_spl_memo"
"kb-lib.decoder.spl.memo"
]
},
{
"name": "file_kb_decoder_spl_memo_info",
"name": "file_kb_lib_decoder_spl_memo_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/devnet/kb_decoder_spl_memo/info.log",
"path": "logs/devnet/kb-lib/decoder/spl/memo/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_decoder_spl_memo"
"kb-lib.decoder.spl.memo"
]
},
{
"name": "file_kb_decoder_spl_memo_error",
"name": "file_kb_lib_decoder_spl_memo_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/devnet/kb_decoder_spl_memo/error.jsonl",
"path": "logs/devnet/kb-lib/decoder/spl/memo/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_decoder_spl_memo"
"kb-lib.decoder.spl.memo"
]
},
{
@@ -1628,7 +1628,7 @@
"kb-lib.decoder.solana.core",
"kb_decoder_spl_associated_token_account",
"kb_decoder_spl_elgamal_registry",
"kb_decoder_spl_memo",
"kb-lib.decoder.spl.memo",
"kb_decoder_spl_token",
"kb_decoder_spl_token_2022",
"kb_execution_solana",
@@ -1788,42 +1788,42 @@
]
},
{
"name": "file_kb_decoder_spl_memo_debug",
"name": "file_kb_lib_decoder_spl_memo_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/mainnet_research/kb_decoder_spl_memo/debug.log",
"path": "logs/mainnet_research/kb-lib/decoder/spl/memo/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_decoder_spl_memo"
"kb-lib.decoder.spl.memo"
]
},
{
"name": "file_kb_decoder_spl_memo_info",
"name": "file_kb_lib_decoder_spl_memo_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/mainnet_research/kb_decoder_spl_memo/info.log",
"path": "logs/mainnet_research/kb-lib/decoder/spl/memo/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_decoder_spl_memo"
"kb-lib.decoder.spl.memo"
]
},
{
"name": "file_kb_decoder_spl_memo_error",
"name": "file_kb_lib_decoder_spl_memo_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/mainnet_research/kb_decoder_spl_memo/error.jsonl",
"path": "logs/mainnet_research/kb-lib/decoder/spl/memo/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_decoder_spl_memo"
"kb-lib.decoder.spl.memo"
]
},
{
@@ -3430,7 +3430,7 @@
"kb-lib.decoder.solana.core",
"kb_decoder_spl_associated_token_account",
"kb_decoder_spl_elgamal_registry",
"kb_decoder_spl_memo",
"kb-lib.decoder.spl.memo",
"kb_decoder_spl_token",
"kb_decoder_spl_token_2022",
"kb_execution_solana",
@@ -3590,42 +3590,42 @@
]
},
{
"name": "file_kb_decoder_spl_memo_debug",
"name": "file_kb_lib_decoder_spl_memo_debug",
"enabled": true,
"sink": "file",
"level": "debug",
"path": "logs/mainnet/kb_decoder_spl_memo/debug.log",
"path": "logs/mainnet/kb-lib/decoder/spl/memo/debug.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_decoder_spl_memo"
"kb-lib.decoder.spl.memo"
]
},
{
"name": "file_kb_decoder_spl_memo_info",
"name": "file_kb_lib_decoder_spl_memo_info",
"enabled": true,
"sink": "file",
"level": "info",
"path": "logs/mainnet/kb_decoder_spl_memo/info.log",
"path": "logs/mainnet/kb-lib/decoder/spl/memo/info.log",
"rotation": "daily",
"format": "human",
"ansi": false,
"targets": [
"kb_decoder_spl_memo"
"kb-lib.decoder.spl.memo"
]
},
{
"name": "file_kb_decoder_spl_memo_error",
"name": "file_kb_lib_decoder_spl_memo_error",
"enabled": true,
"sink": "file",
"level": "error",
"path": "logs/mainnet/kb_decoder_spl_memo/error.jsonl",
"path": "logs/mainnet/kb-lib/decoder/spl/memo/error.jsonl",
"rotation": "daily",
"format": "json",
"ansi": false,
"targets": [
"kb_decoder_spl_memo"
"kb-lib.decoder.spl.memo"
]
},
{

View File

@@ -1,8 +1,15 @@
{
"file": "docs/SPL_MEMO_MATRIX.json",
"version": 8,
"version": 9,
"matrixVersion": 4,
"scope": "SPL Memo v1, v3 et v4",
"bot3Migration": {
"decoderModule": "kb_lib::decoder::spl::memo",
"publicDecoder": "kb_lib::SplMemoDecoder",
"decoderStatus": "ported_in_0.1.0-pre.005",
"materializerStatus": "pending_later_tranche",
"executorStatus": "pending_later_tranche"
},
"interface": {
"crate": "spl-memo-interface",
"requestedVersion": "^2.1",

View File

@@ -1,5 +1,5 @@
<!-- file: docs/TRACING_CONTRACT.md -->
<!-- version: 13 -->
<!-- version: 14 -->
# Contrat de tracing par crate et composant
@@ -57,6 +57,7 @@ Les targets déjà migrés vers les noms canoniques bot3 sont :
```text
kb-lib.decoder.solana.core
kb-lib.decoder.spl.memo
kb-logging
kb-store
```

View File

@@ -1,5 +1,5 @@
# file: kb-lib/Cargo.toml
# version: 3
# version: 4
[package]
name = "kb-lib"
@@ -29,5 +29,8 @@ tracing.workspace = true
ts-rs.workspace = true
wincode.workspace = true
[dev-dependencies]
spl-memo-interface.workspace = true
[lints]
workspace = true

View File

@@ -1,5 +1,5 @@
<!-- file: kb-lib/README.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# kb-lib
@@ -23,9 +23,16 @@ Il couvre les 18 surfaces natives suivantes :
La matrice `docs/NATIVE_SOLANA_DECODER_MATRIX.json` reste la source machine-readable de la couverture. Les instructions inconnues, tronquées, historiques ou issues dune transaction échouée conservent les statuts et diagnostics explicites définis par les contrats communs.
## Décodeur SPL Memo
`SplMemoDecoder` couvre exactement les générations v1, v3 et v4. Le payload est lu comme un message brut sans discriminator, borné à 4 096 octets, puis projeté avec son texte UTF-8 complet, sa longueur, son SHA-256, les comptes ordonnés et le statut de commit.
Memo v1 ignore les comptes lors de la validation runtime. Memo v3 et v4 exigent que chaque compte fourni soit signer. Les transactions échouées restent des intentions non commitées et les payloads UTF-8 invalides ou signatures manquantes deviennent des tentatives invalides explicites. La matrice normative est `docs/SPL_MEMO_MATRIX.json`.
## API publique utile
- `SolanaCoreDecoder` : décodeur concret natif ;
- `SplMemoDecoder` : décodeur exact des trois générations SPL Memo ;
- `InstructionDecoder` : contrat de reconnaissance, couverture et décodage contextualisé ;
- `ProtocolDecoder` : contrat de compatibilité avec les observations historiques ;
- `CoreInstructionReplayInput` : input source-neutral produit par lextraction core ;

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/decoder.rs
// version: 1
// version: 2
//! Consolidated decoder modules.
@@ -32,3 +32,25 @@ pub mod vault;
pub mod vesting;
pub mod wallet;
pub mod weighted;
/// Maximum payload prefix retained for bounded diagnostics.
pub(crate) use self::spl::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
/// Current stable Memo decoded event contract version.
pub(crate) use self::spl::SPL_MEMO_EVENT_VERSION;
/// Maximum decoded instruction data retained as a complete Memo payload.
pub(crate) use self::spl::SPL_MEMO_MAX_PAYLOAD_BYTES;
/// Stable protocol code shared by Memo generations.
pub(crate) use self::spl::SPL_MEMO_PROTOCOL_CODE;
/// Canonical tracing target for the SPL Memo decoder.
pub(crate) use self::spl::SPL_MEMO_TRACING_TARGET;
/// Stable Memo v1 surface code.
pub(crate) use self::spl::SPL_MEMO_V1_SURFACE_CODE;
/// Stable Memo v3 surface code.
pub(crate) use self::spl::SPL_MEMO_V3_SURFACE_CODE;
/// Stable Memo v4 surface code.
pub(crate) use self::spl::SPL_MEMO_V4_SURFACE_CODE;
/// Decodes one bounded SPL Memo instruction.
pub(crate) use self::spl::spl_memo_decode;
/// Exact decoder for the three registered SPL Memo generations.
pub use self::spl::SplMemoDecoder;

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/decoder/spl.rs
// version: 1
// version: 2
//! `spl` decoder family.
@@ -12,3 +12,25 @@ pub mod single_pool;
pub mod stake_pool;
pub mod token;
pub mod token_2022;
/// Maximum payload prefix retained for bounded diagnostics.
pub(crate) use self::memo::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
/// Current stable Memo decoded event contract version.
pub(crate) use self::memo::SPL_MEMO_EVENT_VERSION;
/// Maximum decoded instruction data retained as a complete Memo payload.
pub(crate) use self::memo::SPL_MEMO_MAX_PAYLOAD_BYTES;
/// Stable protocol code shared by Memo generations.
pub(crate) use self::memo::SPL_MEMO_PROTOCOL_CODE;
/// Canonical tracing target for the SPL Memo decoder.
pub(crate) use self::memo::SPL_MEMO_TRACING_TARGET;
/// Stable Memo v1 surface code.
pub(crate) use self::memo::SPL_MEMO_V1_SURFACE_CODE;
/// Stable Memo v3 surface code.
pub(crate) use self::memo::SPL_MEMO_V3_SURFACE_CODE;
/// Stable Memo v4 surface code.
pub(crate) use self::memo::SPL_MEMO_V4_SURFACE_CODE;
/// Decodes one bounded SPL Memo instruction.
pub(crate) use self::memo::spl_memo_decode;
/// Exact decoder for the three registered SPL Memo generations.
pub use self::memo::SplMemoDecoder;

View File

@@ -1,10 +1,30 @@
// file: kb-lib/src/decoder/spl/memo.rs
// version: 1
// version: 2
//! Migration boundary for legacy crate `kb_decoder_spl_memo`.
//! Exact SPL Memo v1, v3 and v4 decoder component.
/// Legacy crate name retained for migration and compatibility tracking.
pub const LEGACY_CRATE: &str = "kb_decoder_spl_memo";
mod constants;
mod decoder;
mod payload;
/// Current porting status.
pub const MIGRATION_STATUS: &str = "source-preserved-pending-port";
/// Maximum payload prefix retained for bounded diagnostics.
pub(crate) use self::constants::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
/// Current stable Memo decoded event contract version.
pub(crate) use self::constants::SPL_MEMO_EVENT_VERSION;
/// Maximum decoded instruction data retained as a complete Memo payload.
pub(crate) use self::constants::SPL_MEMO_MAX_PAYLOAD_BYTES;
/// Stable protocol code shared by Memo generations.
pub(crate) use self::constants::SPL_MEMO_PROTOCOL_CODE;
/// Canonical tracing target for the SPL Memo decoder.
pub(crate) use self::constants::SPL_MEMO_TRACING_TARGET;
/// Stable Memo v1 surface code.
pub(crate) use self::constants::SPL_MEMO_V1_SURFACE_CODE;
/// Stable Memo v3 surface code.
pub(crate) use self::constants::SPL_MEMO_V3_SURFACE_CODE;
/// Stable Memo v4 surface code.
pub(crate) use self::constants::SPL_MEMO_V4_SURFACE_CODE;
/// Decodes one bounded SPL Memo instruction.
pub(crate) use self::payload::decode as spl_memo_decode;
/// Exact decoder for the three registered SPL Memo generations.
pub use self::decoder::SplMemoDecoder;

View File

@@ -0,0 +1,21 @@
// file: kb-lib/src/decoder/spl/memo/constants.rs
// version: 2
//! Local constants for the `kb-lib` SPL Memo component. Program identifiers live in `kb_program_ids`.
/// Stable protocol code shared by Memo generations.
pub(crate) const SPL_MEMO_PROTOCOL_CODE: &str = "spl_memo";
/// Stable Memo v1 surface code.
pub(crate) const SPL_MEMO_V1_SURFACE_CODE: &str = "spl_memo_v1";
/// Stable Memo v3 surface code.
pub(crate) const SPL_MEMO_V3_SURFACE_CODE: &str = "spl_memo_v3";
/// Stable Memo v4 surface code.
pub(crate) const SPL_MEMO_V4_SURFACE_CODE: &str = "spl_memo_v4";
/// Maximum decoded instruction data retained as a complete Memo payload.
pub(crate) const SPL_MEMO_MAX_PAYLOAD_BYTES: usize = 4_096;
/// Maximum payload prefix retained for bounded diagnostics.
pub(crate) const SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES: usize = 32;
/// Current stable Memo decoded event contract version.
pub(crate) const SPL_MEMO_EVENT_VERSION: u32 = 1;
/// Canonical tracing target for this crate.
pub(crate) const SPL_MEMO_TRACING_TARGET: &str = "kb-lib.decoder.spl.memo";

View File

@@ -0,0 +1,429 @@
// file: kb-lib/src/decoder/spl/memo/decoder.rs
// version: 1
//! Exact SPL Memo v1, v3 and v4 dispatch for the common decode pipeline.
const MEMO_SURFACES: &[crate::DecoderSurface] = &[
crate::DecoderSurface {
program_id: kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
surface_code: crate::SPL_MEMO_V1_SURFACE_CODE,
priority: 100,
},
crate::DecoderSurface {
program_id: kb_program_ids::SPL_MEMO_V3_PROGRAM_ID,
surface_code: crate::SPL_MEMO_V3_SURFACE_CODE,
priority: 100,
},
crate::DecoderSurface {
program_id: kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
surface_code: crate::SPL_MEMO_V4_SURFACE_CODE,
priority: 100,
},
];
const LEGACY_PROGRAM_IDS: &[&str] = &[
kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
kb_program_ids::SPL_MEMO_V3_PROGRAM_ID,
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
];
/// Exact decoder for the three registered SPL Memo generations.
#[derive(Clone, Debug, Default)]
pub struct SplMemoDecoder;
impl crate::ProtocolDecoder for crate::SplMemoDecoder {
fn decoder_name(&self) -> &'static str {
return "kb_decoder_spl_memo";
}
fn decoder_version(&self) -> &'static str {
return env!("CARGO_PKG_VERSION");
}
fn program_ids(&self) -> &'static [&'static str] {
return LEGACY_PROGRAM_IDS;
}
fn supports_observation(
&self,
observation: &crate::ProgramObservation,
) -> crate::DecoderSupport {
if crate::ProtocolDecoder::handles_program_id(self, &observation.program_id) {
return crate::DecoderSupport::Yes;
}
return 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::SplMemoDecoder {
fn identity(&self) -> crate::DecoderIdentity {
return crate::DecoderIdentity {
name: "spl_memo".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
};
}
fn surfaces(&self) -> &'static [crate::DecoderSurface] {
return MEMO_SURFACES;
}
fn coverage(&self) -> std::vec::Vec<crate::DecoderCoverageDeclaration> {
return MEMO_SURFACES
.iter()
.map(|surface| {
return crate::DecoderCoverageDeclaration {
program_id: surface.program_id.to_string(),
surface_code: std::option::Option::Some(surface.surface_code.to_string()),
entry_kind: crate::DecoderCoverageEntryKind::Instruction,
entry_code: "add_memo".to_string(),
discriminator_hex: std::option::Option::None,
historical: surface.program_id != kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
};
})
.collect();
}
fn recognize(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderRecognition {
let surface = MEMO_SURFACES
.iter()
.find(|surface| return surface.program_id == input.program_id);
let surface = match surface {
std::option::Option::Some(value) => value,
std::option::Option::None => return crate::DecoderRecognition::incompatible(),
};
return crate::DecoderRecognition::compatible(
true,
surface.priority,
std::option::Option::Some(surface.surface_code.to_string()),
std::option::Option::Some("add_memo".to_string()),
std::option::Option::None,
);
}
fn decode(&self, input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
if !LEGACY_PROGRAM_IDS.contains(&input.program_id.as_str()) {
return crate::DecoderExecutionResult::unsupported(std::option::Option::None);
}
let result = crate::spl_memo_decode(input);
if matches!(
result.status,
crate::DecoderOutcomeStatus::Failed | crate::DecoderOutcomeStatus::Unsupported
) {
tracing::error!(
target: crate::SPL_MEMO_TRACING_TARGET,
action = "decode_failure",
signature = %input.signature,
slot = input.slot,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
processor_name = "spl_memo",
processor_version = env!("CARGO_PKG_VERSION"),
input_key = %input.replay_input_key,
payload_hash = ?input.instruction_payload_hash,
transaction_failed = input.transaction_failed,
result_status = ?result.status,
diagnostics = ?result.diagnostics,
"SPL Memo instruction was not decoded successfully"
);
}
tracing::debug!(
target: crate::SPL_MEMO_TRACING_TARGET,
action = "decode",
signature = %input.signature,
instruction_path = %input.instruction_path,
program_id = %input.program_id,
transaction_failed = input.transaction_failed,
result_status = ?result.status,
observation_count = result.observations.len(),
"SPL Memo 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,
) -> crate::CoreInstructionReplayInput {
let result = crate::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!("memo replay input failed: {error}"),
};
}
fn one_account(signer: bool) -> (serde_json::Value, serde_json::Value) {
return (
serde_json::json!([{"position":0,"accountIndex":1,"accountKey":"signer"}]),
serde_json::json!([{"accountIndex":1,"accountKey":"signer","signer":signer,"writable":false,"source":"static"}]),
);
}
#[test]
fn exact_support_and_interface_ids_cover_all_generations() {
let decoder = crate::SplMemoDecoder;
assert_eq!(spl_memo_interface::v1::ID.to_string(), kb_program_ids::SPL_MEMO_V1_PROGRAM_ID);
assert_eq!(spl_memo_interface::v3::ID.to_string(), kb_program_ids::SPL_MEMO_V3_PROGRAM_ID);
assert_eq!(spl_memo_interface::v4::ID.to_string(), kb_program_ids::SPL_MEMO_V4_PROGRAM_ID);
assert_eq!(crate::InstructionDecoder::surfaces(&decoder).len(), 3);
assert!(
crate::InstructionDecoder::coverage(&decoder)
.iter()
.all(|entry| return entry.entry_code == "add_memo")
);
}
#[test]
fn decodes_empty_ascii_unicode_outer_and_inner_payloads() {
for (index, payload) in [b"".as_slice(), b"payment:42".as_slice(), "memo ".as_bytes()]
.iter()
.enumerate()
{
let path = if index == 2 { "1/0" } else { "0" };
let input = input(
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
payload,
serde_json::json!([]),
serde_json::json!([]),
false,
path,
);
let result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &input);
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
assert_eq!(
result.observations[0].payload_json["payloadLengthBytes"],
serde_json::json!(payload.len())
);
assert!(result.observations[0].observation_committed);
assert_eq!(
result.observations[0].event.source_kind,
if path.contains('/') {
crate::EventSourceKind::InnerInstruction
} else {
crate::EventSourceKind::Instruction
}
);
}
}
#[test]
fn v1_ignores_accounts_while_v3_and_v4_require_every_account_to_sign() {
let (accounts, keys) = one_account(false);
let v1 = input(
kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
b"legacy",
accounts.clone(),
keys.clone(),
false,
"0",
);
let v1_result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &v1);
assert_eq!(v1_result.observations[0].event.event_name.0, "add_memo");
assert_eq!(
v1_result.observations[0].payload_json["signerValidation"]["status"],
"not_applicable"
);
for program_id in
[kb_program_ids::SPL_MEMO_V3_PROGRAM_ID, kb_program_ids::SPL_MEMO_V4_PROGRAM_ID]
{
let input = input(program_id, b"signed", accounts.clone(), keys.clone(), false, "0");
let result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &input);
assert_eq!(result.observations[0].event.event_name.0, "invalid_memo_attempt");
assert_eq!(
result.observations[0].payload_json["signerValidation"]["status"],
"invalid"
);
assert!(!result.observations[0].observation_committed);
}
}
#[test]
fn invalid_utf8_and_failed_transactions_are_explicit_uncommitted_intents() {
let invalid = input(
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
&[0xf0, 0x9f, 0x90, 0xff],
serde_json::json!([]),
serde_json::json!([]),
true,
"0",
);
let invalid_result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &invalid);
assert_eq!(invalid_result.observations[0].event.event_name.0, "invalid_memo_attempt");
assert_eq!(
invalid_result.observations[0].payload_json["utf8Validation"]["status"],
"invalid"
);
assert!(!invalid_result.observations[0].observation_committed);
let valid_failed = input(
kb_program_ids::SPL_MEMO_V3_PROGRAM_ID,
b"rolled back",
serde_json::json!([]),
serde_json::json!([]),
true,
"0",
);
let failed_result =
crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &valid_failed);
assert_eq!(failed_result.observations[0].event.event_name.0, "memo_intent");
assert!(!failed_result.observations[0].observation_committed);
assert_eq!(
failed_result.observations[0].payload_json["runtimeValidation"],
"not_proven_transaction_failed"
);
}
#[test]
fn preserves_duplicate_writable_and_multiple_signer_accounts_in_order() {
let accounts = serde_json::json!([
{"position":0,"accountIndex":1,"accountKey":"first"},
{"position":1,"accountIndex":2,"accountKey":"second"},
{"position":2,"accountIndex":1,"accountKey":"first"}
]);
let keys = serde_json::json!([
{"accountIndex":1,"accountKey":"first","signer":true,"writable":true,"source":"static"},
{"accountIndex":2,"accountKey":"second","signer":true,"writable":false,"source":"static"}
]);
let input =
input(kb_program_ids::SPL_MEMO_V4_PROGRAM_ID, b"ordered", accounts, keys, false, "0");
let result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &input);
assert_eq!(result.observations[0].payload_json["accounts"][0]["pubkey"], "first");
assert_eq!(result.observations[0].payload_json["accounts"][0]["writable"], true);
assert_eq!(result.observations[0].payload_json["accounts"][2]["pubkey"], "first");
assert_eq!(
result.observations[0].payload_json["signerValidation"]["orderedRequiredSigners"],
serde_json::json!(["first", "second", "first"])
);
assert_eq!(
result.observations[0].payload_json["signerValidation"]["orderedObservedSigners"],
serde_json::json!(["first", "second", "first"])
);
}
#[test]
fn payload_hash_is_canonical_sha256() {
let input = input(
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
b"abc",
serde_json::json!([]),
serde_json::json!([]),
false,
"0",
);
let result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &input);
assert_eq!(
result.observations[0].payload_json["payloadSha256"],
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn payload_bound_accepts_limit_and_rejects_oversize_and_malformed_base64() {
let at_limit = std::vec![b'x'; crate::SPL_MEMO_MAX_PAYLOAD_BYTES];
let cinput = input(
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
at_limit.as_slice(),
serde_json::json!([]),
serde_json::json!([]),
false,
"0",
);
let result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &cinput);
assert_eq!(result.status, crate::DecoderOutcomeStatus::Decoded);
let oversize = std::vec![b'x'; crate::SPL_MEMO_MAX_PAYLOAD_BYTES + 1];
let cinput = input(
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
oversize.as_slice(),
serde_json::json!([]),
serde_json::json!([]),
false,
"0",
);
let result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &cinput);
assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed);
let mut malformed = input(
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
b"valid",
serde_json::json!([]),
serde_json::json!([]),
false,
"0",
);
malformed.instruction_payload_json =
std::option::Option::Some(serde_json::json!({"dataBase64":"%%%"}));
let result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &malformed);
assert_eq!(result.status, crate::DecoderOutcomeStatus::Failed);
assert!(result.observations.is_empty());
}
#[test]
fn unknown_program_is_incompatible_and_never_emits_an_observation() {
let input = input(
kb_program_ids::SYSTEM_PROGRAM_ID,
b"not memo",
serde_json::json!([]),
serde_json::json!([]),
false,
"0",
);
let recognition = crate::InstructionDecoder::recognize(&crate::SplMemoDecoder, &input);
assert!(!recognition.compatible);
let result = crate::InstructionDecoder::decode(&crate::SplMemoDecoder, &input);
assert_eq!(result.status, crate::DecoderOutcomeStatus::Unsupported);
assert!(result.observations.is_empty());
}
#[test]
fn matrix_is_machine_readable_and_covers_required_cases() {
let parsed = serde_json::from_str::<serde_json::Value>(include_str!(
"../../../../../docs/SPL_MEMO_MATRIX.json"
));
let matrix = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => panic!("memo matrix is invalid JSON: {error}"),
};
assert_eq!(matrix["matrixVersion"], 4);
assert_eq!(
matrix["generations"].as_array().map(std::vec::Vec::len),
std::option::Option::Some(3)
);
let cases = matrix["requiredCorpusCases"].as_array();
assert!(cases.is_some_and(|values| return values.len() >= 12));
}
}

View File

@@ -0,0 +1,371 @@
// file: kb-lib/src/decoder/spl/memo/payload.rs
// version: 1
//! Bounded Memo payload, account and validation projection.
use base64::Engine; // rust-rules: trait-import
use sha2::Digest; // rust-rules: trait-import
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum MemoGeneration {
V1,
V3,
V4,
}
impl MemoGeneration {
fn from_program_id(program_id: &str) -> std::option::Option<Self> {
if program_id == kb_program_ids::SPL_MEMO_V1_PROGRAM_ID {
return std::option::Option::Some(Self::V1);
}
if program_id == kb_program_ids::SPL_MEMO_V3_PROGRAM_ID {
return std::option::Option::Some(Self::V3);
}
if program_id == kb_program_ids::SPL_MEMO_V4_PROGRAM_ID {
return std::option::Option::Some(Self::V4);
}
return std::option::Option::None;
}
fn code(self) -> &'static str {
return match self {
Self::V1 => "v1",
Self::V3 => "v3",
Self::V4 => "v4",
};
}
fn surface_code(self) -> &'static str {
return match self {
Self::V1 => crate::SPL_MEMO_V1_SURFACE_CODE,
Self::V3 => crate::SPL_MEMO_V3_SURFACE_CODE,
Self::V4 => crate::SPL_MEMO_V4_SURFACE_CODE,
};
}
fn validates_signers(self) -> bool {
return self != Self::V1;
}
}
struct ResolvedAccounts {
values: serde_json::Value,
ordered_accounts: std::vec::Vec<std::string::String>,
ordered_signers: std::vec::Vec<std::string::String>,
missing_signers: std::vec::Vec<std::string::String>,
}
pub(crate) fn decode(input: &crate::CoreInstructionReplayInput) -> crate::DecoderExecutionResult {
let generation = match MemoGeneration::from_program_id(input.program_id.as_str()) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return crate::DecoderExecutionResult::unsupported(std::option::Option::None);
},
};
let bytes = match decode_payload(input) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return failed("memo_payload_unavailable", error.to_string());
},
};
let accounts = match resolve_accounts(input) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return failed("memo_accounts_malformed", error.to_string());
},
};
let text_result = std::str::from_utf8(bytes.as_slice());
let utf8_valid = text_result.is_ok();
let signer_valid = !generation.validates_signers() || accounts.missing_signers.is_empty();
let structurally_valid = utf8_valid && signer_valid;
let event_name = if !structurally_valid {
"invalid_memo_attempt"
} else if input.transaction_failed {
"memo_intent"
} else {
"add_memo"
};
let memo_text = match text_result {
std::result::Result::Ok(value) => serde_json::Value::String(value.to_string()),
std::result::Result::Err(_error) => serde_json::Value::Null,
};
let utf8_error_offset = std::str::from_utf8(bytes.as_slice())
.err()
.map(|error| return error.valid_up_to());
let signer_status = if !generation.validates_signers() {
"not_applicable"
} else if signer_valid {
"valid"
} else {
"invalid"
};
let runtime_validation = if input.transaction_failed {
"not_proven_transaction_failed"
} else if structurally_valid {
"proven_by_successful_transaction"
} else {
"inconsistent_with_successful_transaction"
};
let missing_required_signers = if generation.validates_signers() {
accounts.missing_signers.clone()
} else {
std::vec::Vec::new()
};
let ordered_required_signers = if generation.validates_signers() {
accounts.ordered_accounts.clone()
} else {
std::vec::Vec::new()
};
let payload_hash = hash(bytes.as_slice());
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_MEMO_PROTOCOL_CODE.to_string()),
surface_code: crate::SurfaceCode(generation.surface_code().to_string()),
event_code: crate::EventCode(format!("{}.{event_name}", generation.surface_code())),
event_name: crate::EventName(event_name.to_string()),
event_family: crate::EventFamily::Audit,
source_kind: if input.instruction_path.contains('/') {
crate::EventSourceKind::InnerInstruction
} else {
crate::EventSourceKind::Instruction
},
confidence: crate::DecoderConfidence::ManualExact,
};
let observation = crate::DecodedObservation {
event_key: "memo:0".to_string(),
event,
payload_json: serde_json::json!({
"eventVersion": crate::SPL_MEMO_EVENT_VERSION,
"generation": generation.code(),
"programId": input.program_id,
"instructionPath": input.instruction_path,
"instructionLocation": if input.instruction_path.contains('/') { "inner" } else { "outer" },
"transactionSucceeded": !input.transaction_failed,
"committed": !input.transaction_failed && structurally_valid,
"memoText": memo_text,
"payloadLengthBytes": bytes.len(),
"payloadSha256": payload_hash,
"payloadHexPrefix": hex_prefix(bytes.as_slice()),
"payloadComplete": true,
"utf8Validation": {
"status": if utf8_valid { "valid" } else { "invalid" },
"invalidFromByte": utf8_error_offset
},
"signerValidation": {
"requiredForGeneration": generation.validates_signers(),
"accountsIgnoredByRuntime": !generation.validates_signers(),
"status": signer_status,
"orderedRequiredSigners": ordered_required_signers,
"orderedObservedSigners": accounts.ordered_signers,
"missingRequiredSigners": missing_required_signers
},
"runtimeValidation": runtime_validation,
"accounts": accounts.values,
"diagnostic": if structurally_valid {
serde_json::Value::Null
} else {
serde_json::json!({
"code": if !utf8_valid { "invalid_utf8" } else { "missing_required_signature" },
"payloadHexPrefix": hex_prefix(bytes.as_slice())
})
}
}),
transaction_failed: input.transaction_failed,
transaction_error: input.transaction_err_json.clone(),
observation_committed: !input.transaction_failed && structurally_valid,
proof: crate::DecoderProof {
kind: crate::DecoderProofKind::Manual,
confidence: crate::DecoderConfidence::ManualExact,
evidence: std::vec![
"spl_memo_exact_raw_instruction_data".to_string(),
format!("payload_sha256:{payload_hash}"),
format!("generation:{}", generation.code()),
],
},
};
return crate::DecoderExecutionResult {
status: crate::DecoderOutcomeStatus::Decoded,
recognized_entry_code: std::option::Option::Some("add_memo".to_string()),
observations: std::vec![observation],
diagnostics: std::vec::Vec::new(),
};
}
fn decode_payload(input: &crate::CoreInstructionReplayInput) -> kb_core::Result<std::vec::Vec<u8>> {
let payload = match input.instruction_payload_json.as_ref() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::invalid_state(
"Memo instruction payload is not retained",
));
},
};
let encoded = match payload.get("dataBase64").and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::invalid_state(
"Memo instruction payload does not contain dataBase64",
));
},
};
let maximum_encoded = crate::SPL_MEMO_MAX_PAYLOAD_BYTES
.saturating_mul(4)
.saturating_div(3)
.saturating_add(4);
if encoded.len() > maximum_encoded {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"Memo base64 payload exceeds {maximum_encoded} bytes"
)));
}
let decoded = match base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"Memo payload is not valid base64: {error}"
)));
},
};
if decoded.len() > crate::SPL_MEMO_MAX_PAYLOAD_BYTES {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"Memo payload exceeds {} decoded bytes",
crate::SPL_MEMO_MAX_PAYLOAD_BYTES
)));
}
return std::result::Result::Ok(decoded);
}
fn resolve_accounts(
input: &crate::CoreInstructionReplayInput,
) -> kb_core::Result<ResolvedAccounts> {
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(
"Memo instruction accounts must be a JSON array",
));
},
};
let account_keys = match input.account_keys_json.as_array() {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::invalid_state(
"Memo transaction account keys must be a JSON array",
));
},
};
let mut values = std::vec::Vec::with_capacity(instruction_accounts.len());
let mut ordered_accounts = std::vec::Vec::with_capacity(instruction_accounts.len());
let mut ordered_signers = std::vec::Vec::new();
let mut missing_signers = std::vec::Vec::new();
for (position, account) in instruction_accounts.iter().enumerate() {
let account_index = match account.get("accountIndex").and_then(serde_json::Value::as_u64) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"Memo instruction account {position} has no accountIndex"
)));
},
};
let account_key = match account.get("accountKey").and_then(serde_json::Value::as_str) {
std::option::Option::Some(value) if !value.trim().is_empty() => value,
_ => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"Memo instruction account {position} has no accountKey"
)));
},
};
let resolved = account_keys.iter().find(|candidate| {
return candidate.get("accountIndex").and_then(serde_json::Value::as_u64)
== std::option::Option::Some(account_index);
});
let resolved = match resolved {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"Memo account index {account_index} is absent from resolved keys"
)));
},
};
if resolved.get("accountKey").and_then(serde_json::Value::as_str)
!= std::option::Option::Some(account_key)
{
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"Memo account index {account_index} resolves to a different key"
)));
}
let signer = match resolved.get("signer").and_then(serde_json::Value::as_bool) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"Memo account index {account_index} has no signer flag"
)));
},
};
let writable = match resolved.get("writable").and_then(serde_json::Value::as_bool) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(kb_core::Error::invalid_state(format!(
"Memo account index {account_index} has no writable flag"
)));
},
};
ordered_accounts.push(account_key.to_string());
if signer {
ordered_signers.push(account_key.to_string());
} else {
missing_signers.push(account_key.to_string());
}
let source = match resolved.get("source") {
std::option::Option::Some(value) => value.clone(),
std::option::Option::None => serde_json::Value::Null,
};
values.push(serde_json::json!({
"position": position,
"accountIndex": account_index,
"pubkey": account_key,
"signer": signer,
"writable": writable,
"source": source
}));
}
return std::result::Result::Ok(ResolvedAccounts {
values: serde_json::Value::Array(values),
ordered_accounts,
ordered_signers,
missing_signers,
});
}
fn hash(bytes: &[u8]) -> std::string::String {
let digest = sha2::Sha256::digest(bytes);
let mut output = std::string::String::with_capacity(64);
for byte in digest {
output.push_str(format!("{byte:02x}").as_str());
}
return output;
}
fn hex_prefix(bytes: &[u8]) -> std::string::String {
let length = std::cmp::min(bytes.len(), crate::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES);
let mut output = std::string::String::with_capacity(length.saturating_mul(2));
for byte in &bytes[..length] {
output.push_str(format!("{byte:02x}").as_str());
}
return output;
}
fn failed(code: &str, message: std::string::String) -> crate::DecoderExecutionResult {
return crate::DecoderExecutionResult {
status: crate::DecoderOutcomeStatus::Failed,
recognized_entry_code: std::option::Option::Some("add_memo".to_string()),
observations: std::vec::Vec::new(),
diagnostics: std::vec![crate::DecoderDiagnostic {
code: code.to_string(),
message,
retriable: false,
}],
};
}

View File

@@ -1,5 +1,5 @@
// file: kb-lib/src/lib.rs
// version: 6
// version: 7
//! Consolidated decoder, executor, materializer and shared model library.
#![warn(missing_docs)]
@@ -11,6 +11,22 @@ pub mod executor;
pub mod materializer;
pub mod model;
/// Maximum payload prefix retained for bounded Memo diagnostics.
pub(crate) use crate::decoder::SPL_MEMO_DIAGNOSTIC_PREFIX_BYTES;
/// Current stable Memo decoded event contract version.
pub(crate) use crate::decoder::SPL_MEMO_EVENT_VERSION;
/// Maximum decoded instruction data retained as a complete Memo payload.
pub(crate) use crate::decoder::SPL_MEMO_MAX_PAYLOAD_BYTES;
/// Stable protocol code shared by Memo generations.
pub(crate) use crate::decoder::SPL_MEMO_PROTOCOL_CODE;
/// Canonical tracing target for the SPL Memo decoder.
pub(crate) use crate::decoder::SPL_MEMO_TRACING_TARGET;
/// Stable Memo v1 surface code.
pub(crate) use crate::decoder::SPL_MEMO_V1_SURFACE_CODE;
/// Stable Memo v3 surface code.
pub(crate) use crate::decoder::SPL_MEMO_V3_SURFACE_CODE;
/// Stable Memo v4 surface code.
pub(crate) use crate::decoder::SPL_MEMO_V4_SURFACE_CODE;
/// Stable Address Lookup Table surface code.
pub(crate) use crate::decoder::solana::SOLANA_CORE_ADDRESS_LOOKUP_TABLE_SURFACE_CODE;
/// Stable deprecated immutable BPF Loader surface code.
@@ -61,6 +77,8 @@ pub(crate) use crate::decoder::solana::solana_core_config_decode;
pub(crate) use crate::decoder::solana::solana_core_config_recognize;
/// Resolves positional instruction accounts and validates their core indexes.
pub(crate) use crate::decoder::solana::solana_core_resolve_accounts;
/// Decodes one bounded SPL Memo instruction.
pub(crate) use crate::decoder::spl_memo_decode;
/// Stable protocol code shared by native Solana events.
pub(crate) use crate::decoder::solana::SOLANA_CORE_PROTOCOL_CODE;
@@ -184,6 +202,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.
pub(crate) use crate::decoder::solana::solana_core_zk_token_proof_recognize;
/// Exact decoder for the three registered SPL Memo generations.
pub use crate::decoder::SplMemoDecoder;
/// Current contextual core instruction input contract version.
pub use crate::decoder::api::contracts::CORE_INSTRUCTION_INPUT_CONTRACT_VERSION;
/// Stable contextual decoded observation.

View File

@@ -1,11 +1,11 @@
<!-- file: kb-program-ids/README.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# kb-program-ids
`kb-program-ids` est la source unique des identifiants de programmes Solana et des comptes natifs connus utilisés par le workspace.
La tranche `0.1.0-pre.004` expose les 18 surfaces exécutables nécessaires au décodeur Solana Core, les comptes natifs associés utilisés par leurs contrats et lidentifiant SPL Token classique employé par les tests de frontière. 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 lidentifiant 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. Les identifiants SPL et protocolaires supplémentaires seront ajoutés avec leurs tranches de migration respectives.
## API publique

View File

@@ -1,5 +1,5 @@
// file: kb-program-ids/src/constants.rs
// version: 1
// version: 2
//! Canonical Solana program and well-known account identifiers.
@@ -31,6 +31,12 @@ pub const SECP256K1_PROGRAM_ID: &str = "KeccakSecp256k11111111111111111111111111
pub const SECP256R1_PROGRAM_ID: &str = "Secp256r1SigVerify1111111111111111111111111";
/// Enshrined stateless Slashing program identifier.
pub const SLASHING_PROGRAM_ID: &str = "S1ashing11111111111111111111111111111111111";
/// Historical SPL Memo v1 program identifier.
pub const SPL_MEMO_V1_PROGRAM_ID: &str = "Memo1UhkJRfHyvLMcVucJwxXeuD728EqVDDwQDxFMNo";
/// Historical SPL Memo v3 program identifier.
pub const SPL_MEMO_V3_PROGRAM_ID: &str = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
/// Current SPL Memo v4 program identifier.
pub const SPL_MEMO_V4_PROGRAM_ID: &str = "Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH";
/// Classic SPL Token program identifier.
pub const SPL_TOKEN_PROGRAM_ID: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
/// Historical Stake configuration account identifier.
@@ -201,6 +207,18 @@ pub(crate) const PROGRAM_ID_ENTRIES: &[crate::ProgramIdEntry] = &[
name: "slashing",
address: crate::SLASHING_PROGRAM_ID,
},
crate::ProgramIdEntry {
name: "spl_memo_v1",
address: crate::SPL_MEMO_V1_PROGRAM_ID,
},
crate::ProgramIdEntry {
name: "spl_memo_v3",
address: crate::SPL_MEMO_V3_PROGRAM_ID,
},
crate::ProgramIdEntry {
name: "spl_memo_v4",
address: crate::SPL_MEMO_V4_PROGRAM_ID,
},
crate::ProgramIdEntry {
name: "spl_token",
address: crate::SPL_TOKEN_PROGRAM_ID,

View File

@@ -1,5 +1,5 @@
// file: kb-program-ids/src/lib.rs
// version: 2
// version: 3
#![forbid(unsafe_code)]
#![deny(unreachable_pub)]
@@ -43,6 +43,12 @@ pub use constants::SECP256K1_PROGRAM_ID;
pub use constants::SECP256R1_PROGRAM_ID;
/// Enshrined stateless Slashing program identifier.
pub use constants::SLASHING_PROGRAM_ID;
/// Historical SPL Memo v1 program identifier.
pub use constants::SPL_MEMO_V1_PROGRAM_ID;
/// Historical SPL Memo v3 program identifier.
pub use constants::SPL_MEMO_V3_PROGRAM_ID;
/// Current SPL Memo v4 program identifier.
pub use constants::SPL_MEMO_V4_PROGRAM_ID;
/// Classic SPL Token program identifier.
pub use constants::SPL_TOKEN_PROGRAM_ID;
/// Historical Stake configuration account identifier.
@@ -132,7 +138,7 @@ mod tests {
assert!(codes.insert(entry.code()));
assert!(program_ids.insert(entry.program_id()));
}
assert_eq!(crate::registered_program_ids().len(), 26);
assert_eq!(crate::registered_program_ids().len(), 29);
}
#[test]
@@ -187,4 +193,21 @@ mod tests {
assert!(crate::find_registered_program_id(program_id).is_some());
}
}
#[test]
fn spl_memo_generations_are_registered_without_joining_native_surfaces() {
let memo_program_ids = [
crate::SPL_MEMO_V1_PROGRAM_ID,
crate::SPL_MEMO_V3_PROGRAM_ID,
crate::SPL_MEMO_V4_PROGRAM_ID,
];
for program_id in memo_program_ids {
assert!(crate::find_registered_program_id(program_id).is_some());
assert!(
!crate::native_program_ids()
.iter()
.any(|entry| return entry.program_id() == program_id)
);
}
}
}