solana_transaction_status/
extract_memos.rs

1use {
2    crate::{parse_instruction::parse_memo_data, VersionedTransactionWithStatusMeta},
3    solana_message::{
4        compiled_instruction::CompiledInstruction, AccountKeys, Message, SanitizedMessage,
5    },
6    solana_pubkey::Pubkey,
7};
8
9pub fn extract_and_fmt_memos<T: ExtractMemos>(message: &T) -> Option<String> {
10    let memos = message.extract_memos();
11    if memos.is_empty() {
12        None
13    } else {
14        Some(memos.join("; "))
15    }
16}
17
18fn extract_and_fmt_memo_data(data: &[u8]) -> String {
19    let memo_len = data.len();
20    let parsed_memo = parse_memo_data(data).unwrap_or_else(|_| "(unparseable)".to_string());
21    format!("[{memo_len}] {parsed_memo}")
22}
23
24pub trait ExtractMemos {
25    fn extract_memos(&self) -> Vec<String>;
26}
27
28impl ExtractMemos for Message {
29    fn extract_memos(&self) -> Vec<String> {
30        extract_memos_inner(
31            &AccountKeys::new(&self.account_keys, None),
32            &self.instructions,
33        )
34    }
35}
36
37impl ExtractMemos for SanitizedMessage {
38    fn extract_memos(&self) -> Vec<String> {
39        extract_memos_inner(&self.account_keys(), self.instructions())
40    }
41}
42
43impl ExtractMemos for VersionedTransactionWithStatusMeta {
44    fn extract_memos(&self) -> Vec<String> {
45        extract_memos_inner(
46            &self.account_keys(),
47            self.transaction.message.instructions(),
48        )
49    }
50}
51
52enum KeyType<'a> {
53    MemoProgram,
54    OtherProgram,
55    Unknown(&'a Pubkey),
56}
57
58fn extract_memos_inner(
59    account_keys: &AccountKeys,
60    instructions: &[CompiledInstruction],
61) -> Vec<String> {
62    let mut account_keys: Vec<KeyType> = account_keys.iter().map(KeyType::Unknown).collect();
63    instructions
64        .iter()
65        .filter_map(|ix| {
66            let index = ix.program_id_index as usize;
67            let key_type = account_keys.get(index)?;
68            let memo_data = match key_type {
69                KeyType::MemoProgram => Some(&ix.data),
70                KeyType::OtherProgram => None,
71                KeyType::Unknown(program_id) => {
72                    if **program_id == spl_memo_interface::v1::id()
73                        || **program_id == spl_memo_interface::v3::id()
74                    {
75                        account_keys[index] = KeyType::MemoProgram;
76                        Some(&ix.data)
77                    } else {
78                        account_keys[index] = KeyType::OtherProgram;
79                        None
80                    }
81                }
82            }?;
83            Some(extract_and_fmt_memo_data(memo_data))
84        })
85        .collect()
86}
87
88#[cfg(test)]
89mod test {
90    use super::*;
91
92    #[test]
93    fn test_extract_memos_inner() {
94        let fee_payer = Pubkey::new_unique();
95        let another_program_id = Pubkey::new_unique();
96        let memo0 = "Test memo";
97        let memo1 = "🦖";
98        let expected_memos = vec![
99            format!("[{}] {}", memo0.len(), memo0),
100            format!("[{}] {}", memo1.len(), memo1),
101        ];
102        let memo_instructions = vec![
103            CompiledInstruction {
104                program_id_index: 1,
105                accounts: vec![],
106                data: memo0.as_bytes().to_vec(),
107            },
108            CompiledInstruction {
109                program_id_index: 2,
110                accounts: vec![],
111                data: memo1.as_bytes().to_vec(),
112            },
113            CompiledInstruction {
114                program_id_index: 3,
115                accounts: vec![],
116                data: memo1.as_bytes().to_vec(),
117            },
118        ];
119        let static_keys = vec![
120            fee_payer,
121            spl_memo_interface::v1::id(),
122            another_program_id,
123            spl_memo_interface::v3::id(),
124        ];
125        let account_keys = AccountKeys::new(&static_keys, None);
126
127        assert_eq!(
128            extract_memos_inner(&account_keys, &memo_instructions),
129            expected_memos
130        );
131    }
132}