use solana_message::compiled_instruction::CompiledInstruction;
use solana_pubkey::Pubkey;
use crate::exact::{SPL_MEMO_PROGRAM, SolanaExactError};
pub(crate) fn verify_memo(
instructions: &[CompiledInstruction],
static_keys: &[Pubkey],
expected: Option<&str>,
) -> Result<(), SolanaExactError> {
let Some(expected) = expected else {
return Ok(());
};
let memo_instructions: Vec<&CompiledInstruction> = instructions
.iter()
.filter(|ix| {
static_keys
.get(usize::from(ix.program_id_index))
.is_some_and(|pk| *pk == SPL_MEMO_PROGRAM)
})
.collect();
let [only] = memo_instructions.as_slice() else {
return Err(SolanaExactError::MemoInstructionCountInvalid {
count: memo_instructions.len(),
});
};
if only.data.as_slice() != expected.as_bytes() {
return Err(SolanaExactError::MemoMismatch);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_ix(program_id_index: u8, data: &[u8]) -> CompiledInstruction {
CompiledInstruction {
program_id_index,
accounts: Vec::new(),
data: data.to_vec(),
}
}
fn keys() -> Vec<Pubkey> {
let payer = Pubkey::new_from_array([1u8; 32]);
vec![payer, SPL_MEMO_PROGRAM]
}
#[test]
fn no_requirement_accepts_any() {
assert!(verify_memo(&[], &keys(), None).is_ok());
let ix = make_ix(1, b"arbitrary");
assert!(verify_memo(&[ix], &keys(), None).is_ok());
}
#[test]
fn missing_memo_rejected() {
assert!(matches!(
verify_memo(&[], &keys(), Some("order-123")),
Err(SolanaExactError::MemoInstructionCountInvalid { count: 0 }),
));
}
#[test]
fn mismatched_memo_rejected() {
let ix = make_ix(1, b"wrong");
assert!(matches!(
verify_memo(&[ix], &keys(), Some("order-123")),
Err(SolanaExactError::MemoMismatch),
));
}
#[test]
fn matching_memo_accepted() {
let ix = make_ix(1, b"order-123");
assert!(verify_memo(&[ix], &keys(), Some("order-123")).is_ok());
}
#[test]
fn multiple_memos_rejected() {
let ix = make_ix(1, b"order-123");
let ixs = vec![ix.clone(), ix];
assert!(matches!(
verify_memo(&ixs, &keys(), Some("order-123")),
Err(SolanaExactError::MemoInstructionCountInvalid { count: 2 }),
));
}
}