0.1.0
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
# file: kb_executor_spl_memo/Cargo.toml
|
||||
# version: 3
|
||||
|
||||
[package]
|
||||
name = "kb_executor_spl_memo"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[dependencies]
|
||||
kb_core = { path = "../kb_core" }
|
||||
kb_execution_api = { path = "../kb_execution_api" }
|
||||
kb_model = { path = "../kb_model" }
|
||||
kb_program_ids = { path = "../kb_program_ids" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
solana-instruction.workspace = true
|
||||
solana-pubkey.workspace = true
|
||||
spl-memo-interface.workspace = true
|
||||
tracing.workspace = true
|
||||
ts-rs.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
kb_execution_safety = { path = "../kb_execution_safety" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,50 @@
|
||||
<!-- file: kb_executor_spl_memo/README.md -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# kb_executor_spl_memo
|
||||
|
||||
Ce crate reconnaît les Program IDs exacts SPL Memo v1, v3 et v4, mais construit des plans d’exécution uniquement pour la génération courante v4. Il ne dépend ni du wallet, ni du RPC, ni du décodeur et n’effectue directement aucune signature ni aucun envoi.
|
||||
|
||||
## Contrat typé
|
||||
|
||||
L’unique opération est `spl_memo.add_memo`, portée par :
|
||||
|
||||
- `SplMemoGeneration::V1`, `V3` ou `V4`, afin de produire une capacité exacte ;
|
||||
- `SplMemoOperation::AddMemo` ;
|
||||
- `SplMemoExecutionIntent` avec intent ID, fee payer et politique commune ;
|
||||
- une liste ordonnée de `SplMemoSigner` dont les doublons sont conservés dans l’instruction.
|
||||
|
||||
Le message est un `String` Rust : sa validité UTF‑8 est donc acquise avant la construction. Les octets UTF‑8 exacts deviennent l’intégralité de `Instruction::data`, sans préfixe, discriminator, Borsh ou `bincode`.
|
||||
|
||||
## Builder et comptes
|
||||
|
||||
Pour v4, le crate appelle directement `spl_memo_interface::instruction::build_memo` de la version 2.1.0 avec le Program ID explicite. Les tests comparent le plan produit au builder officiel. Les générations v1 et v3 sont historiques `decode-only` et n'atteignent jamais ce builder par l'API de l'exécuteur.
|
||||
|
||||
Chaque signer fourni devient un compte `readonly`, `is_signer = true`, dans l’ordre exact. Aucun compte writable n’est inventé. Les occurrences dupliquées restent dans les metas ; la liste transactionnelle `required_signers` est dédupliquée et commence toujours par le fee payer.
|
||||
|
||||
Le payload est borné à 566 octets et la liste à 32 occurrences de signataires. Ces limites sont des garde-fous conservateurs de l’exécuteur, pas de nouveaux rejets attribués au runtime. L’assembleur commun vérifie ensuite la taille wire exacte de 1 232 octets, qui dépend notamment du nombre de signatures.
|
||||
|
||||
## Capacités et politiques
|
||||
|
||||
La construction de plan est `Supported` pour `AddMemo` uniquement sur l'ID v4 exact. V1 et v3 retournent `Unsupported(reason)` avec le code stable `execution_spl_memo_historical_generation_decode_only`. Toute autre opération ou tout autre Program ID est également `Unsupported(reason)` avec son propre motif.
|
||||
|
||||
- simulation obligatoire ;
|
||||
- dry-run par défaut via `ExecutionPolicy::default()` ;
|
||||
- plafond de frais strictement positif obligatoire ;
|
||||
- dépense d’instruction égale à zéro lamport, hors frais réseau ;
|
||||
- construction v4 disponible sur Localnet, Devnet, Testnet et Mainnet ;
|
||||
- aucune génération artificiellement limitée au dry-run par la bibliothèque ;
|
||||
- Mainnet gouverné par la politique commune `allow_mainnet` et sa confirmation explicite ;
|
||||
- insertion canonique, extraction core, decode replay Memo et matérialisation exigés après exécution.
|
||||
|
||||
La disponibilité effective de v4 sur un cluster n’est pas supposée par le builder : elle est établie par la simulation RPC obligatoire. Une future génération expérimentale officiellement encodée pourra être ajoutée sans la limiter artificiellement à un cluster. Les générations historiques restent décodables dans `kb_decoder_spl_memo`, mais ne sont pas exécutables. La démo `0.4.3` reste limitée à Devnet.
|
||||
|
||||
Les signataires restent visibles dans `PreparedExecutionPlan` avant toute signature. Les clés privées ne font partie ni de l’intent ni du plan.
|
||||
|
||||
## Validation Devnet et démos
|
||||
|
||||
`kb_pipeline::execute_devnet_memo` réutilise l’infrastructure de `0.4.2` pour construire un Memo v4, simuler, demander une autorisation explicite avant l’envoi, confirmer, hydrater, extraire, rejouer le décodeur, vérifier l’annotation puis prouver l’idempotence par un second replay non forcé.
|
||||
|
||||
La fenêtre Solana Devnet existante expose ce parcours sans nouvelle WebView. Elle affiche payload, fee payer, signataire wallet readonly, plan, simulation et validation post-exécution. Le journal générique `demo_decode_replay` reste la surface de consultation des annotations matérialisées. Les DTO Memo sérialisent soldes et frais en chaînes décimales et n’exportent aucune clé privée ni `bigint` actif.
|
||||
|
||||
La validation réelle du 15 juillet 2026 comprend une simulation seule et trois transactions v4 Devnet confirmées. Chacune a produit une insertion canonique, une extraction core, un decode, une annotation et un second replay `skipped` sans nouvelle matérialisation. Les signatures exactes sont enregistrées dans `docs/SPL_MEMO_MATRIX.json`.
|
||||
@@ -0,0 +1,501 @@
|
||||
// file: kb_executor_spl_memo/src/builder.rs
|
||||
// version: 6
|
||||
|
||||
//! Official SPL Memo instruction builder and conservative plan validation.
|
||||
|
||||
use std::str::FromStr; // rust-rules: trait-import
|
||||
|
||||
pub(crate) fn build_prepared_plan(
|
||||
intent: &crate::SplMemoExecutionIntent,
|
||||
) -> kb_core::Result<kb_execution_api::PreparedExecutionPlan> {
|
||||
if intent.intent_id.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_intent_id_empty",
|
||||
"SPL Memo execution intent id must not be empty",
|
||||
));
|
||||
}
|
||||
let fee_payer = match parse_pubkey(&intent.fee_payer, "fee_payer") {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
match validate_policy(intent) {
|
||||
std::result::Result::Ok(()) => {},
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
let (generation, message, signers) = match &intent.operation {
|
||||
crate::SplMemoOperation::AddMemo { generation, message, signers } => {
|
||||
(*generation, message, signers)
|
||||
},
|
||||
};
|
||||
if message.len() > crate::MAX_MEMO_MESSAGE_BYTES {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_payload_too_large",
|
||||
format!(
|
||||
"SPL Memo payload length {} exceeds the executor limit {}",
|
||||
message.len(),
|
||||
crate::MAX_MEMO_MESSAGE_BYTES
|
||||
),
|
||||
));
|
||||
}
|
||||
if signers.len() > crate::MAX_MEMO_SIGNERS {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_signer_limit_exceeded",
|
||||
format!(
|
||||
"SPL Memo signer occurrence count {} exceeds the executor limit {}",
|
||||
signers.len(),
|
||||
crate::MAX_MEMO_SIGNERS
|
||||
),
|
||||
));
|
||||
}
|
||||
let program_id = match solana_pubkey::Pubkey::from_str(generation.program_id()) {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_program_id_invalid",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let mut signer_pubkeys = std::vec::Vec::with_capacity(signers.len());
|
||||
for signer in signers {
|
||||
let pubkey = match parse_pubkey(&signer.pubkey, "memo_signer") {
|
||||
std::result::Result::Ok(pubkey) => pubkey,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
signer_pubkeys.push(pubkey);
|
||||
}
|
||||
let signer_refs = signer_pubkeys.iter().collect::<std::vec::Vec<_>>();
|
||||
let instruction = spl_memo_interface::instruction::build_memo(
|
||||
&program_id,
|
||||
message.as_bytes(),
|
||||
signer_refs.as_slice(),
|
||||
);
|
||||
let planned_instruction = planned_instruction(crate::SPL_MEMO_ADD_MEMO_OPERATION, &instruction);
|
||||
let required_signers = required_signers(&intent.fee_payer, signers.as_slice());
|
||||
tracing::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "build_prepared_plan",
|
||||
intent_id = %intent.intent_id,
|
||||
operation_code = crate::SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
program_id = generation.program_id(),
|
||||
payload_length = message.len(),
|
||||
signer_occurrence_count = signers.len(),
|
||||
required_signer_count = required_signers.len(),
|
||||
dry_run = intent.policy.dry_run,
|
||||
"built SPL Memo execution plan"
|
||||
);
|
||||
return std::result::Result::Ok(kb_execution_api::PreparedExecutionPlan {
|
||||
executor_name: std::string::String::from("kb_executor_spl_memo"),
|
||||
executor_version: std::string::String::from(env!("CARGO_PKG_VERSION")),
|
||||
intent_id: intent.intent_id.clone(),
|
||||
operation_code: std::string::String::from(crate::SPL_MEMO_ADD_MEMO_OPERATION),
|
||||
fee_payer: kb_model::Pubkey(fee_payer.to_string()),
|
||||
instructions: vec![planned_instruction],
|
||||
required_signers,
|
||||
policy: intent.policy.clone(),
|
||||
requested_spend_lamports: 0,
|
||||
requested_compute_unit_price_micro_lamports: std::option::Option::None,
|
||||
});
|
||||
}
|
||||
|
||||
fn validate_policy(intent: &crate::SplMemoExecutionIntent) -> kb_core::Result<()> {
|
||||
if intent.policy.simulation != kb_execution_api::ExecutionSimulationPolicy::Required {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_simulation_required",
|
||||
"SPL Memo execution requires simulation before signing or sending",
|
||||
));
|
||||
}
|
||||
match intent.policy.cost_limit.max_fee_lamports {
|
||||
std::option::Option::Some(limit) if limit > 0 => {},
|
||||
std::option::Option::Some(_) | std::option::Option::None => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_fee_limit_missing",
|
||||
"SPL Memo execution requires a positive transaction fee ceiling",
|
||||
));
|
||||
},
|
||||
}
|
||||
let validation = &intent.policy.post_execution_validation;
|
||||
if !validation.canonical_insert_required
|
||||
|| !validation.core_extraction_required
|
||||
|| !validation.decode_replay_required
|
||||
|| !validation.materialization_required
|
||||
{
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_post_validation_required",
|
||||
"SPL Memo execution requires canonical insertion, core extraction, decode replay and materialization validation",
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn parse_pubkey(pubkey: &kb_model::Pubkey, field: &str) -> kb_core::Result<solana_pubkey::Pubkey> {
|
||||
if pubkey.0.trim().is_empty() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_pubkey_empty",
|
||||
format!("SPL Memo {field} public key must not be empty"),
|
||||
));
|
||||
}
|
||||
return match solana_pubkey::Pubkey::from_str(pubkey.0.as_str()) {
|
||||
std::result::Result::Ok(parsed) => std::result::Result::Ok(parsed),
|
||||
std::result::Result::Err(error) => std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_pubkey_invalid",
|
||||
format!("invalid SPL Memo {field} public key: {error}"),
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
fn planned_instruction(
|
||||
operation_code: &str,
|
||||
instruction: &solana_instruction::Instruction,
|
||||
) -> kb_execution_api::PlannedInstruction {
|
||||
return kb_execution_api::PlannedInstruction {
|
||||
program_id: kb_model::ProgramId(instruction.program_id.to_string()),
|
||||
operation_code: std::string::String::from(operation_code),
|
||||
accounts: instruction
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|account| {
|
||||
return kb_execution_api::PlannedAccount {
|
||||
pubkey: kb_model::Pubkey(account.pubkey.to_string()),
|
||||
is_signer: account.is_signer,
|
||||
is_writable: account.is_writable,
|
||||
};
|
||||
})
|
||||
.collect(),
|
||||
data: instruction.data.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
fn required_signers(
|
||||
fee_payer: &kb_model::Pubkey,
|
||||
signers: &[crate::SplMemoSigner],
|
||||
) -> std::vec::Vec<kb_execution_api::RequiredSigner> {
|
||||
let mut required = vec![kb_execution_api::RequiredSigner {
|
||||
pubkey: fee_payer.clone(),
|
||||
role: std::string::String::from("fee_payer"),
|
||||
}];
|
||||
for signer in signers {
|
||||
if required.iter().any(|candidate| return candidate.pubkey == signer.pubkey) {
|
||||
continue;
|
||||
}
|
||||
required.push(kb_execution_api::RequiredSigner {
|
||||
pubkey: signer.pubkey.clone(),
|
||||
role: std::string::String::from("memo_signer"),
|
||||
});
|
||||
}
|
||||
return required;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::str::FromStr; // rust-rules: trait-import
|
||||
|
||||
fn pubkey(value: &str) -> kb_model::Pubkey {
|
||||
return kb_model::Pubkey(std::string::String::from(value));
|
||||
}
|
||||
|
||||
fn intent(
|
||||
generation: crate::SplMemoGeneration,
|
||||
message: &str,
|
||||
signers: &[&str],
|
||||
dry_run: bool,
|
||||
) -> crate::SplMemoExecutionIntent {
|
||||
let fee_payer = kb_program_ids::SYSTEM_PROGRAM_ID;
|
||||
let mut authorized_signers = vec![pubkey(fee_payer)];
|
||||
for signer in signers {
|
||||
let candidate = pubkey(signer);
|
||||
if !authorized_signers.contains(&candidate) {
|
||||
authorized_signers.push(candidate);
|
||||
}
|
||||
}
|
||||
return crate::SplMemoExecutionIntent {
|
||||
intent_id: std::string::String::from("memo-intent-1"),
|
||||
fee_payer: pubkey(fee_payer),
|
||||
policy: kb_execution_api::ExecutionPolicy {
|
||||
cost_limit: kb_execution_api::ExecutionCostLimit {
|
||||
max_spend_lamports: std::option::Option::Some(0),
|
||||
max_fee_lamports: std::option::Option::Some(10_000),
|
||||
max_compute_unit_price_micro_lamports: std::option::Option::None,
|
||||
},
|
||||
authorized_signers,
|
||||
dry_run,
|
||||
post_execution_validation: kb_execution_api::PostExecutionValidationPolicy {
|
||||
canonical_insert_required: true,
|
||||
core_extraction_required: true,
|
||||
decode_replay_required: true,
|
||||
materialization_required: true,
|
||||
},
|
||||
..kb_execution_api::ExecutionPolicy::default()
|
||||
},
|
||||
operation: crate::SplMemoOperation::AddMemo {
|
||||
generation,
|
||||
message: std::string::String::from(message),
|
||||
signers: signers
|
||||
.iter()
|
||||
.map(|value| {
|
||||
return crate::SplMemoSigner { pubkey: pubkey(value) };
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn assert_matches_official(
|
||||
plan: &kb_execution_api::PreparedExecutionPlan,
|
||||
generation: crate::SplMemoGeneration,
|
||||
message: &str,
|
||||
signers: &[&str],
|
||||
) {
|
||||
let program_id = solana_pubkey::Pubkey::from_str(generation.program_id())
|
||||
.unwrap_or_else(|error| panic!("invalid program fixture: {error}"));
|
||||
let signer_pubkeys = signers
|
||||
.iter()
|
||||
.map(|value| {
|
||||
return solana_pubkey::Pubkey::from_str(value)
|
||||
.unwrap_or_else(|error| panic!("invalid signer fixture: {error}"));
|
||||
})
|
||||
.collect::<std::vec::Vec<_>>();
|
||||
let signer_refs = signer_pubkeys.iter().collect::<std::vec::Vec<_>>();
|
||||
let official = spl_memo_interface::instruction::build_memo(
|
||||
&program_id,
|
||||
message.as_bytes(),
|
||||
signer_refs.as_slice(),
|
||||
);
|
||||
assert_eq!(plan.instructions.len(), 1);
|
||||
let actual = &plan.instructions[0];
|
||||
assert_eq!(actual.program_id.0, official.program_id.to_string());
|
||||
assert_eq!(actual.data, official.data);
|
||||
assert_eq!(actual.accounts.len(), official.accounts.len());
|
||||
for (actual_account, official_account) in
|
||||
actual.accounts.iter().zip(official.accounts.iter())
|
||||
{
|
||||
assert_eq!(actual_account.pubkey.0, official_account.pubkey.to_string());
|
||||
assert_eq!(actual_account.is_signer, official_account.is_signer);
|
||||
assert_eq!(actual_account.is_writable, official_account.is_writable);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_generation_matches_the_official_explicit_program_builder() {
|
||||
let generation = crate::SplMemoGeneration::V4;
|
||||
let intent = intent(generation, "memo 🐆", &[kb_program_ids::VOTE_PROGRAM_ID], true);
|
||||
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("Memo plan failed: {error}"));
|
||||
assert_matches_official(&plan, generation, "memo 🐆", &[kb_program_ids::VOTE_PROGRAM_ID]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_payload_and_zero_signers_are_constructible() {
|
||||
let intent = intent(crate::SplMemoGeneration::V4, "", &[], false);
|
||||
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("empty Memo plan failed: {error}"));
|
||||
assert!(plan.instructions[0].data.is_empty());
|
||||
assert!(plan.instructions[0].accounts.is_empty());
|
||||
assert_eq!(plan.required_signers.len(), 1);
|
||||
assert_eq!(plan.requested_spend_lamports, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_signers_remain_ordered_readonly_accounts_but_required_signers_are_unique() {
|
||||
let first = kb_program_ids::VOTE_PROGRAM_ID;
|
||||
let second = kb_program_ids::STAKE_PROGRAM_ID;
|
||||
let intent =
|
||||
intent(crate::SplMemoGeneration::V4, "ordered", &[first, second, first], false);
|
||||
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("ordered Memo plan failed: {error}"));
|
||||
let accounts = &plan.instructions[0].accounts;
|
||||
assert_eq!(accounts.len(), 3);
|
||||
assert_eq!(accounts[0].pubkey.0, first);
|
||||
assert_eq!(accounts[1].pubkey.0, second);
|
||||
assert_eq!(accounts[2].pubkey.0, first);
|
||||
assert!(accounts.iter().all(|account| return account.is_signer));
|
||||
assert!(accounts.iter().all(|account| return !account.is_writable));
|
||||
assert_eq!(plan.required_signers.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prepared_plan_passes_common_pre_simulation_safety() {
|
||||
let intent = intent(
|
||||
crate::SplMemoGeneration::V4,
|
||||
"safe plan",
|
||||
&[kb_program_ids::VOTE_PROGRAM_ID],
|
||||
true,
|
||||
);
|
||||
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("safe Memo plan failed: {error}"));
|
||||
let evaluation = kb_execution_safety::ExecutionSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("safety evaluation failed: {error}"));
|
||||
assert_eq!(evaluation.decision, kb_execution_safety::ExecutionSafetyDecision::Allow);
|
||||
assert!(evaluation.violations.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_generation_accepts_non_dry_run_plans() {
|
||||
let generation = crate::SplMemoGeneration::V4;
|
||||
let intent = intent(generation, "dry", &[], false);
|
||||
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("non-dry-run Memo plan failed: {error}"));
|
||||
assert!(!plan.policy.dry_run);
|
||||
assert_eq!(plan.instructions[0].program_id.0, generation.program_id());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_oversize_payload_and_optional_simulation() {
|
||||
let mut oversize = intent(
|
||||
crate::SplMemoGeneration::V4,
|
||||
std::string::String::from_utf8(vec![b'a'; crate::MAX_MEMO_MESSAGE_BYTES + 1])
|
||||
.unwrap_or_else(|error| panic!("fixture creation failed: {error}"))
|
||||
.as_str(),
|
||||
&[],
|
||||
true,
|
||||
);
|
||||
let error = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&oversize,
|
||||
)
|
||||
.expect_err("oversize payload must fail");
|
||||
assert_eq!(error.code(), "execution_spl_memo_payload_too_large");
|
||||
|
||||
oversize.operation = crate::SplMemoOperation::AddMemo {
|
||||
generation: crate::SplMemoGeneration::V4,
|
||||
message: std::string::String::from("valid"),
|
||||
signers: std::vec::Vec::new(),
|
||||
};
|
||||
oversize.policy.simulation = kb_execution_api::ExecutionSimulationPolicy::Optional;
|
||||
let error = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&oversize,
|
||||
)
|
||||
.expect_err("optional simulation must fail");
|
||||
assert_eq!(error.code(), "execution_spl_memo_simulation_required");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_cluster_is_constructible_and_mainnet_remains_governed_by_common_safety() {
|
||||
for cluster in [
|
||||
kb_execution_api::ExecutionCluster::Localnet,
|
||||
kb_execution_api::ExecutionCluster::Devnet,
|
||||
kb_execution_api::ExecutionCluster::Testnet,
|
||||
kb_execution_api::ExecutionCluster::Mainnet,
|
||||
] {
|
||||
let mut intent = intent(crate::SplMemoGeneration::V4, "universal", &[], false);
|
||||
intent.policy.cluster.expected_cluster = cluster;
|
||||
let plan = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("cluster plan failed: {error}"));
|
||||
assert_eq!(plan.policy.cluster.expected_cluster, cluster);
|
||||
if cluster == kb_execution_api::ExecutionCluster::Mainnet {
|
||||
let evaluation = kb_execution_safety::ExecutionSafetyChecker
|
||||
.evaluate_prepared_plan(&plan)
|
||||
.unwrap_or_else(|error| panic!("Mainnet safety evaluation failed: {error}"));
|
||||
assert_eq!(evaluation.decision, kb_execution_safety::ExecutionSafetyDecision::Deny);
|
||||
assert!(evaluation.violations.iter().any(|violation| {
|
||||
return violation.code == "execution_mainnet_disabled";
|
||||
}));
|
||||
let mut explicitly_enabled = plan.clone();
|
||||
explicitly_enabled.policy.cluster.allow_mainnet = true;
|
||||
explicitly_enabled.policy.cluster.mainnet_confirmation = true;
|
||||
let enabled_evaluation = kb_execution_safety::ExecutionSafetyChecker
|
||||
.evaluate_prepared_plan(&explicitly_enabled)
|
||||
.unwrap_or_else(|error| {
|
||||
panic!("enabled Mainnet safety evaluation failed: {error}")
|
||||
});
|
||||
assert_eq!(
|
||||
enabled_evaluation.decision,
|
||||
kb_execution_safety::ExecutionSafetyDecision::Allow
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_fee_cap_incomplete_post_validation_and_signer_overflow() {
|
||||
let mut intent = intent(crate::SplMemoGeneration::V4, "policy", &[], true);
|
||||
intent.policy.cost_limit.max_fee_lamports = std::option::Option::None;
|
||||
let error = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.expect_err("missing fee cap must fail");
|
||||
assert_eq!(error.code(), "execution_spl_memo_fee_limit_missing");
|
||||
|
||||
intent.policy.cost_limit.max_fee_lamports = std::option::Option::Some(10_000);
|
||||
intent.policy.post_execution_validation.materialization_required = false;
|
||||
let error = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.expect_err("incomplete post validation must fail");
|
||||
assert_eq!(error.code(), "execution_spl_memo_post_validation_required");
|
||||
|
||||
intent.policy.post_execution_validation.materialization_required = true;
|
||||
intent.operation = crate::SplMemoOperation::AddMemo {
|
||||
generation: crate::SplMemoGeneration::V4,
|
||||
message: std::string::String::from("bounded"),
|
||||
signers: (0..=crate::MAX_MEMO_SIGNERS)
|
||||
.map(|_| {
|
||||
return crate::SplMemoSigner {
|
||||
pubkey: pubkey(kb_program_ids::VOTE_PROGRAM_ID),
|
||||
};
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
let error = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.expect_err("signer overflow must fail");
|
||||
assert_eq!(error.code(), "execution_spl_memo_signer_limit_exceeded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn machine_readable_matrix_matches_executor_policy() {
|
||||
let parsed = serde_json::from_str::<serde_json::Value>(include_str!(
|
||||
"../../docs/SPL_MEMO_MATRIX.json"
|
||||
))
|
||||
.unwrap_or_else(|error| panic!("Memo matrix parsing failed: {error}"));
|
||||
let contract = parsed
|
||||
.get("executorContract")
|
||||
.unwrap_or_else(|| panic!("Memo executor contract is missing"));
|
||||
assert_eq!(
|
||||
contract.get("operationCode").and_then(serde_json::Value::as_str),
|
||||
std::option::Option::Some(crate::SPL_MEMO_ADD_MEMO_OPERATION)
|
||||
);
|
||||
assert_eq!(
|
||||
contract.get("maximumMessageBytes").and_then(serde_json::Value::as_u64),
|
||||
std::option::Option::Some(crate::MAX_MEMO_MESSAGE_BYTES as u64)
|
||||
);
|
||||
assert_eq!(
|
||||
contract.get("maximumSignerOccurrences").and_then(serde_json::Value::as_u64),
|
||||
std::option::Option::Some(crate::MAX_MEMO_SIGNERS as u64)
|
||||
);
|
||||
assert_eq!(
|
||||
contract.get("allClustersConstructible").and_then(serde_json::Value::as_bool),
|
||||
std::option::Option::Some(true)
|
||||
);
|
||||
assert_eq!(
|
||||
contract
|
||||
.get("historicalGenerationSendEnabled")
|
||||
.and_then(serde_json::Value::as_bool),
|
||||
std::option::Option::Some(false)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// file: kb_executor_spl_memo/src/constants.rs
|
||||
// version: 2
|
||||
|
||||
//! Local constants for the `kb_executor_spl_memo` crate. Program identifiers live in `kb_program_ids`.
|
||||
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb_executor_spl_memo";
|
||||
@@ -0,0 +1,318 @@
|
||||
// file: kb_executor_spl_memo/src/executor.rs
|
||||
// version: 4
|
||||
|
||||
//! Exact capability dispatch and typed plan construction for SPL Memo.
|
||||
|
||||
/// SPL Memo executor implementation.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SplMemoExecutor;
|
||||
|
||||
impl crate::SplMemoExecutor {
|
||||
fn exact_capability(
|
||||
&self,
|
||||
program_id: &kb_model::ProgramId,
|
||||
operation_code: &str,
|
||||
) -> kb_execution_api::ExecutionCapability {
|
||||
let generation = match crate::SplMemoGeneration::from_program_id(program_id.0.as_str()) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return kb_execution_api::ExecutionCapability::unsupported(
|
||||
"execution_spl_memo_program_not_owned",
|
||||
format!("program {} is not owned by kb_executor_spl_memo", program_id.0),
|
||||
);
|
||||
},
|
||||
};
|
||||
if operation_code != crate::SPL_MEMO_ADD_MEMO_OPERATION {
|
||||
return kb_execution_api::ExecutionCapability::unsupported(
|
||||
"execution_spl_memo_operation_unsupported",
|
||||
format!("SPL Memo operation {operation_code} is not implemented"),
|
||||
);
|
||||
}
|
||||
if generation != crate::SplMemoGeneration::V4 {
|
||||
return kb_execution_api::ExecutionCapability::unsupported(
|
||||
"execution_spl_memo_historical_generation_decode_only",
|
||||
format!(
|
||||
"SPL Memo generation {generation:?} is historical decode-only; only current or experimental generations are executable"
|
||||
),
|
||||
);
|
||||
}
|
||||
return kb_execution_api::ExecutionCapability::supported(operation_code);
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_execution_api::TypedInstructionExecutor for crate::SplMemoExecutor {
|
||||
type Intent = crate::SplMemoExecutionIntent;
|
||||
|
||||
fn capability(
|
||||
&self,
|
||||
program_id: &kb_model::ProgramId,
|
||||
operation_code: &str,
|
||||
) -> kb_execution_api::ExecutionCapability {
|
||||
return self.exact_capability(program_id, operation_code);
|
||||
}
|
||||
|
||||
fn build_prepared_plan(
|
||||
&self,
|
||||
intent: &Self::Intent,
|
||||
) -> kb_core::Result<kb_execution_api::PreparedExecutionPlan> {
|
||||
let program_id = kb_model::ProgramId(std::string::String::from(
|
||||
intent.operation.generation().program_id(),
|
||||
));
|
||||
return match self.exact_capability(&program_id, intent.operation.operation_code()) {
|
||||
kb_execution_api::ExecutionCapability::Supported { operation_code: _ } => {
|
||||
crate::build_prepared_plan(intent)
|
||||
},
|
||||
kb_execution_api::ExecutionCapability::Unsupported { reason_code, reason } => {
|
||||
std::result::Result::Err(kb_core::Error::new(reason_code, reason))
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_execution_api::InstructionExecutor for crate::SplMemoExecutor {
|
||||
fn executor_name(&self) -> &'static str {
|
||||
return "kb_executor_spl_memo";
|
||||
}
|
||||
|
||||
fn executor_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn program_ids(&self) -> &'static [&'static str] {
|
||||
return &[
|
||||
kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
|
||||
kb_program_ids::SPL_MEMO_V3_PROGRAM_ID,
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
];
|
||||
}
|
||||
|
||||
fn supports_request(
|
||||
&self,
|
||||
request: &kb_execution_api::ExecutionRequest,
|
||||
) -> kb_execution_api::ExecutionSupport {
|
||||
return match self.exact_capability(&request.program_id, &request.operation_code) {
|
||||
kb_execution_api::ExecutionCapability::Supported { operation_code: _ } => {
|
||||
kb_execution_api::ExecutionSupport::Yes
|
||||
},
|
||||
kb_execution_api::ExecutionCapability::Unsupported { reason_code: _, reason: _ } => {
|
||||
kb_execution_api::ExecutionSupport::No
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn build_plan(
|
||||
&self,
|
||||
request: &kb_execution_api::ExecutionRequest,
|
||||
) -> kb_core::Result<kb_execution_api::ExecutionPlan> {
|
||||
match self.exact_capability(&request.program_id, &request.operation_code) {
|
||||
kb_execution_api::ExecutionCapability::Supported { operation_code: _ } => {},
|
||||
kb_execution_api::ExecutionCapability::Unsupported { reason_code, reason } => {
|
||||
return std::result::Result::Err(kb_core::Error::new(reason_code, reason));
|
||||
},
|
||||
}
|
||||
let intent =
|
||||
match serde_json::from_str::<crate::SplMemoExecutionIntent>(&request.payload_json) {
|
||||
std::result::Result::Ok(intent) => intent,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_intent_deserialize_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
if intent.operation.operation_code() != request.operation_code.as_str() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_operation_code_mismatch",
|
||||
format!(
|
||||
"request operation {} does not match typed intent operation {}",
|
||||
request.operation_code,
|
||||
intent.operation.operation_code()
|
||||
),
|
||||
));
|
||||
}
|
||||
if intent.operation.generation().program_id() != request.program_id.0.as_str() {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_program_id_mismatch",
|
||||
format!(
|
||||
"request program {} does not match typed intent program {}",
|
||||
request.program_id.0,
|
||||
intent.operation.generation().program_id()
|
||||
),
|
||||
));
|
||||
}
|
||||
let prepared =
|
||||
match kb_execution_api::TypedInstructionExecutor::build_prepared_plan(self, &intent) {
|
||||
std::result::Result::Ok(plan) => plan,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let payload_value = match serde_json::to_value(&prepared) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(kb_core::Error::new(
|
||||
"execution_spl_memo_plan_serialize_failed",
|
||||
error.to_string(),
|
||||
));
|
||||
},
|
||||
};
|
||||
let payload_json = match kb_execution_api::serialize_payload_json(&payload_value) {
|
||||
std::result::Result::Ok(serialized) => serialized,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return std::result::Result::Ok(kb_execution_api::ExecutionPlan {
|
||||
executor_name: std::string::String::from("kb_executor_spl_memo"),
|
||||
instruction_count: prepared.instructions.len(),
|
||||
payload_json,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
fn request(
|
||||
program_id: &str,
|
||||
operation_code: &str,
|
||||
payload_json: std::string::String,
|
||||
) -> kb_execution_api::ExecutionRequest {
|
||||
return kb_execution_api::ExecutionRequest {
|
||||
program_id: kb_model::ProgramId(program_id.to_string()),
|
||||
operation_code: operation_code.to_string(),
|
||||
payload_json,
|
||||
};
|
||||
}
|
||||
|
||||
fn pubkey(value: &str) -> kb_model::Pubkey {
|
||||
return kb_model::Pubkey(std::string::String::from(value));
|
||||
}
|
||||
|
||||
fn intent(
|
||||
generation: crate::SplMemoGeneration,
|
||||
message: &str,
|
||||
signers: &[&str],
|
||||
dry_run: bool,
|
||||
) -> crate::SplMemoExecutionIntent {
|
||||
let fee_payer = kb_program_ids::SYSTEM_PROGRAM_ID;
|
||||
let mut authorized_signers = vec![pubkey(fee_payer)];
|
||||
for signer in signers {
|
||||
let candidate = pubkey(signer);
|
||||
if !authorized_signers.contains(&candidate) {
|
||||
authorized_signers.push(candidate);
|
||||
}
|
||||
}
|
||||
return crate::SplMemoExecutionIntent {
|
||||
intent_id: std::string::String::from("memo-intent-1"),
|
||||
fee_payer: pubkey(fee_payer),
|
||||
policy: kb_execution_api::ExecutionPolicy {
|
||||
cost_limit: kb_execution_api::ExecutionCostLimit {
|
||||
max_spend_lamports: std::option::Option::Some(0),
|
||||
max_fee_lamports: std::option::Option::Some(10_000),
|
||||
max_compute_unit_price_micro_lamports: std::option::Option::None,
|
||||
},
|
||||
authorized_signers,
|
||||
dry_run,
|
||||
post_execution_validation: kb_execution_api::PostExecutionValidationPolicy {
|
||||
canonical_insert_required: true,
|
||||
core_extraction_required: true,
|
||||
decode_replay_required: true,
|
||||
materialization_required: true,
|
||||
},
|
||||
..kb_execution_api::ExecutionPolicy::default()
|
||||
},
|
||||
operation: crate::SplMemoOperation::AddMemo {
|
||||
generation,
|
||||
message: std::string::String::from(message),
|
||||
signers: signers
|
||||
.iter()
|
||||
.map(|value| {
|
||||
return crate::SplMemoSigner { pubkey: pubkey(value) };
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_capabilities_support_only_current_v4_add_memo() {
|
||||
let executor = crate::SplMemoExecutor;
|
||||
for program_id in
|
||||
[kb_program_ids::SPL_MEMO_V1_PROGRAM_ID, kb_program_ids::SPL_MEMO_V3_PROGRAM_ID]
|
||||
{
|
||||
let request = request(
|
||||
program_id,
|
||||
crate::SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
std::string::String::from("{}"),
|
||||
);
|
||||
assert_eq!(
|
||||
kb_execution_api::InstructionExecutor::supports_request(&executor, &request),
|
||||
kb_execution_api::ExecutionSupport::No
|
||||
);
|
||||
}
|
||||
let current = request(
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
crate::SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
std::string::String::from("{}"),
|
||||
);
|
||||
assert_eq!(
|
||||
kb_execution_api::InstructionExecutor::supports_request(&executor, ¤t),
|
||||
kb_execution_api::ExecutionSupport::Yes
|
||||
);
|
||||
let unsupported_operation = request(
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
"spl_memo.remove_memo",
|
||||
std::string::String::from("{}"),
|
||||
);
|
||||
assert_eq!(
|
||||
kb_execution_api::InstructionExecutor::supports_request(
|
||||
&executor,
|
||||
&unsupported_operation,
|
||||
),
|
||||
kb_execution_api::ExecutionSupport::No
|
||||
);
|
||||
let foreign = request(
|
||||
kb_program_ids::SYSTEM_PROGRAM_ID,
|
||||
crate::SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
std::string::String::from("{}"),
|
||||
);
|
||||
assert_eq!(
|
||||
kb_execution_api::InstructionExecutor::supports_request(&executor, &foreign),
|
||||
kb_execution_api::ExecutionSupport::No
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_request_roundtrips_the_typed_plan() {
|
||||
let intent = intent(
|
||||
crate::SplMemoGeneration::V4,
|
||||
"generic",
|
||||
&[kb_program_ids::VOTE_PROGRAM_ID],
|
||||
false,
|
||||
);
|
||||
let payload_json = serde_json::to_string(&intent)
|
||||
.unwrap_or_else(|error| panic!("intent serialization failed: {error}"));
|
||||
let request = request(
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
crate::SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
payload_json,
|
||||
);
|
||||
let plan =
|
||||
kb_execution_api::InstructionExecutor::build_plan(&crate::SplMemoExecutor, &request)
|
||||
.unwrap_or_else(|error| panic!("generic plan failed: {error}"));
|
||||
assert_eq!(plan.instruction_count, 1);
|
||||
let prepared =
|
||||
serde_json::from_str::<kb_execution_api::PreparedExecutionPlan>(&plan.payload_json)
|
||||
.unwrap_or_else(|error| panic!("prepared plan parsing failed: {error}"));
|
||||
assert_eq!(prepared.instructions[0].data, b"generic");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn historical_generations_are_decode_only_with_stable_reason() {
|
||||
for generation in [crate::SplMemoGeneration::V1, crate::SplMemoGeneration::V3] {
|
||||
let intent = intent(generation, "historical", &[], true);
|
||||
let error = kb_execution_api::TypedInstructionExecutor::build_prepared_plan(
|
||||
&crate::SplMemoExecutor,
|
||||
&intent,
|
||||
)
|
||||
.expect_err("historical Memo generation must not build a plan");
|
||||
assert_eq!(error.code(), "execution_spl_memo_historical_generation_decode_only");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
// file: kb_executor_spl_memo/src/intent.rs
|
||||
// version: 3
|
||||
|
||||
//! Typed SPL Memo execution intents.
|
||||
|
||||
use ts_rs::TS; // rust-rules: derive-import
|
||||
|
||||
/// Stable operation code for adding one SPL Memo annotation.
|
||||
pub const SPL_MEMO_ADD_MEMO_OPERATION: &str = "spl_memo.add_memo";
|
||||
/// Conservative UTF-8 payload bound used before transaction assembly.
|
||||
pub const MAX_MEMO_MESSAGE_BYTES: usize = 566;
|
||||
/// Conservative bound on ordered signer occurrences accepted by one intent.
|
||||
pub const MAX_MEMO_SIGNERS: usize = 32;
|
||||
|
||||
/// Exact SPL Memo program generation.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_executor_spl_memo/intent/SplMemoGeneration.ts"
|
||||
)]
|
||||
pub enum SplMemoGeneration {
|
||||
/// Historical v1 program, whose runtime ignores supplied accounts.
|
||||
V1,
|
||||
/// Historical v3 program, whose runtime requires every supplied account to sign.
|
||||
V3,
|
||||
/// Current v4 program, whose runtime requires every supplied account to sign.
|
||||
V4,
|
||||
}
|
||||
|
||||
impl crate::SplMemoGeneration {
|
||||
/// Returns the exact Program ID for this generation.
|
||||
pub fn program_id(&self) -> &'static str {
|
||||
return match self {
|
||||
Self::V1 => kb_program_ids::SPL_MEMO_V1_PROGRAM_ID,
|
||||
Self::V3 => kb_program_ids::SPL_MEMO_V3_PROGRAM_ID,
|
||||
Self::V4 => kb_program_ids::SPL_MEMO_V4_PROGRAM_ID,
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn from_program_id(program_id: &str) -> std::option::Option<Self> {
|
||||
return match program_id {
|
||||
kb_program_ids::SPL_MEMO_V1_PROGRAM_ID => std::option::Option::Some(Self::V1),
|
||||
kb_program_ids::SPL_MEMO_V3_PROGRAM_ID => std::option::Option::Some(Self::V3),
|
||||
kb_program_ids::SPL_MEMO_V4_PROGRAM_ID => std::option::Option::Some(Self::V4),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// One ordered signer account supplied to the Memo instruction.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_executor_spl_memo/intent/SplMemoSigner.ts"
|
||||
)]
|
||||
pub struct SplMemoSigner {
|
||||
/// Signer public key; duplicates are preserved in instruction order.
|
||||
pub pubkey: kb_model::Pubkey,
|
||||
}
|
||||
|
||||
/// Typed SPL Memo operation arguments.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[serde(tag = "operation", rename_all = "snake_case")]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_executor_spl_memo/intent/SplMemoOperation.ts"
|
||||
)]
|
||||
pub enum SplMemoOperation {
|
||||
/// Add one exact UTF-8 Memo payload with ordered readonly signer accounts.
|
||||
AddMemo {
|
||||
/// Exact Memo program generation.
|
||||
generation: crate::SplMemoGeneration,
|
||||
/// UTF-8 text whose bytes become the complete instruction payload.
|
||||
message: std::string::String,
|
||||
/// Ordered signer accounts. Duplicates are preserved.
|
||||
signers: std::vec::Vec<crate::SplMemoSigner>,
|
||||
},
|
||||
}
|
||||
|
||||
impl crate::SplMemoOperation {
|
||||
/// Returns the stable operation code.
|
||||
pub fn operation_code(&self) -> &'static str {
|
||||
return match self {
|
||||
Self::AddMemo { .. } => crate::SPL_MEMO_ADD_MEMO_OPERATION,
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns the exact requested program generation.
|
||||
pub fn generation(&self) -> crate::SplMemoGeneration {
|
||||
return match self {
|
||||
Self::AddMemo { generation, .. } => *generation,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete typed intent accepted by the SPL Memo executor.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize, TS)]
|
||||
#[ts(
|
||||
export,
|
||||
export_to = "../frontend/ts/bindings/kb_executor_spl_memo/intent/SplMemoExecutionIntent.ts"
|
||||
)]
|
||||
pub struct SplMemoExecutionIntent {
|
||||
/// Stable caller-provided identifier used for logs and replay correlation.
|
||||
pub intent_id: std::string::String,
|
||||
/// Transaction fee payer.
|
||||
pub fee_payer: kb_model::Pubkey,
|
||||
/// Conservative execution policy.
|
||||
pub policy: kb_execution_api::ExecutionPolicy,
|
||||
/// Typed Memo operation and arguments.
|
||||
pub operation: crate::SplMemoOperation,
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// file: kb_executor_spl_memo/src/lib.rs
|
||||
// version: 6
|
||||
|
||||
//! Executor crate for `spl_memo`.
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod builder;
|
||||
mod constants;
|
||||
mod executor;
|
||||
mod intent;
|
||||
|
||||
/// Crate-root access to `build_prepared_plan` from `builder`.
|
||||
pub(crate) use crate::builder::build_prepared_plan;
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) use crate::constants::TRACING_TARGET;
|
||||
|
||||
/// Exposes the SPL Memo executor type implemented by this crate.
|
||||
pub use crate::executor::SplMemoExecutor;
|
||||
/// Maximum UTF-8 memo payload accepted by this executor.
|
||||
pub use crate::intent::MAX_MEMO_MESSAGE_BYTES;
|
||||
/// Maximum ordered signer occurrences accepted by this executor.
|
||||
pub use crate::intent::MAX_MEMO_SIGNERS;
|
||||
/// Stable operation code for adding one SPL Memo annotation.
|
||||
pub use crate::intent::SPL_MEMO_ADD_MEMO_OPERATION;
|
||||
/// Exposes the typed SPL Memo execution intent.
|
||||
pub use crate::intent::SplMemoExecutionIntent;
|
||||
/// Exposes the exact SPL Memo program generation.
|
||||
pub use crate::intent::SplMemoGeneration;
|
||||
/// Exposes the typed SPL Memo operation.
|
||||
pub use crate::intent::SplMemoOperation;
|
||||
/// Exposes one ordered SPL Memo signer declaration.
|
||||
pub use crate::intent::SplMemoSigner;
|
||||
Reference in New Issue
Block a user