use {
crate::{VersionedTransactionWithStatusMeta, parse_instruction::parse_memo_data},
solana_message::{
AccountKeys, Message, SanitizedMessage, compiled_instruction::CompiledInstruction,
},
solana_pubkey::Pubkey,
};
pub fn extract_and_fmt_memos<T: ExtractMemos>(message: &T) -> Option<String> {
let memos = message.extract_memos();
if memos.is_empty() {
None
} else {
Some(memos.join("; "))
}
}
fn extract_and_fmt_memo_data(data: &[u8]) -> String {
let memo_len = data.len();
let parsed_memo = parse_memo_data(data).unwrap_or_else(|_| "(unparseable)".to_string());
format!("[{memo_len}] {parsed_memo}")
}
pub trait ExtractMemos {
fn extract_memos(&self) -> Vec<String>;
}
impl ExtractMemos for Message {
fn extract_memos(&self) -> Vec<String> {
extract_memos_inner(
&AccountKeys::new(&self.account_keys, None),
&self.instructions,
)
}
}
impl ExtractMemos for SanitizedMessage {
fn extract_memos(&self) -> Vec<String> {
extract_memos_inner(&self.account_keys(), self.instructions())
}
}
impl ExtractMemos for VersionedTransactionWithStatusMeta {
fn extract_memos(&self) -> Vec<String> {
extract_memos_inner(
&self.account_keys(),
self.transaction.message.instructions(),
)
}
}
enum KeyType<'a> {
MemoProgram,
OtherProgram,
Unknown(&'a Pubkey),
}
fn extract_memos_inner(
account_keys: &AccountKeys,
instructions: &[CompiledInstruction],
) -> Vec<String> {
let mut account_keys: Vec<KeyType> = account_keys.iter().map(KeyType::Unknown).collect();
instructions
.iter()
.filter_map(|ix| {
let index = ix.program_id_index as usize;
let key_type = account_keys.get(index)?;
let memo_data = match key_type {
KeyType::MemoProgram => Some(&ix.data),
KeyType::OtherProgram => None,
KeyType::Unknown(program_id) => {
if **program_id == spl_memo_interface::v1::id()
|| **program_id == spl_memo_interface::v3::id()
|| **program_id == spl_memo_interface::v4::id()
{
account_keys[index] = KeyType::MemoProgram;
Some(&ix.data)
} else {
account_keys[index] = KeyType::OtherProgram;
None
}
}
}?;
Some(extract_and_fmt_memo_data(memo_data))
})
.collect()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_extract_memos_inner() {
let fee_payer = Pubkey::new_unique();
let another_program_id = Pubkey::new_unique();
let memo0 = "Test memo";
let memo1 = "🦖";
let memo2 = "Yet another memo";
let expected_memos = vec![
format!("[{}] {}", memo0.len(), memo0),
format!("[{}] {}", memo1.len(), memo1),
format!("[{}] {}", memo2.len(), memo2),
];
let memo_instructions = vec![
CompiledInstruction {
program_id_index: 1,
accounts: vec![],
data: memo0.as_bytes().to_vec(),
},
CompiledInstruction {
program_id_index: 2,
accounts: vec![],
data: memo1.as_bytes().to_vec(),
},
CompiledInstruction {
program_id_index: 3,
accounts: vec![],
data: memo1.as_bytes().to_vec(),
},
CompiledInstruction {
program_id_index: 4,
accounts: vec![],
data: memo2.as_bytes().to_vec(),
},
];
let static_keys = vec![
fee_payer,
spl_memo_interface::v1::id(),
another_program_id,
spl_memo_interface::v3::id(),
spl_memo_interface::v4::id(),
];
let account_keys = AccountKeys::new(&static_keys, None);
assert_eq!(
extract_memos_inner(&account_keys, &memo_instructions),
expected_memos
);
}
}