solana_transaction_status/
extract_memos.rs

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