solana_transaction_status/
parse_associated_token.rs

1use {
2    crate::parse_instruction::{
3        check_num_accounts, ParsableProgram, ParseInstructionError, ParsedInstructionEnum,
4    },
5    borsh::BorshDeserialize,
6    serde_json::json,
7    solana_message::{compiled_instruction::CompiledInstruction, AccountKeys},
8    spl_associated_token_account_interface::instruction::AssociatedTokenAccountInstruction,
9};
10
11pub fn parse_associated_token(
12    instruction: &CompiledInstruction,
13    account_keys: &AccountKeys,
14) -> Result<ParsedInstructionEnum, ParseInstructionError> {
15    match instruction.accounts.iter().max() {
16        Some(index) if (*index as usize) < account_keys.len() => {}
17        _ => {
18            // Runtime should prevent this from ever happening
19            return Err(ParseInstructionError::InstructionKeyMismatch(
20                ParsableProgram::SplAssociatedTokenAccount,
21            ));
22        }
23    }
24    let ata_instruction = if instruction.data.is_empty() {
25        AssociatedTokenAccountInstruction::Create
26    } else {
27        AssociatedTokenAccountInstruction::try_from_slice(&instruction.data)
28            .map_err(|_| ParseInstructionError::InstructionNotParsable(ParsableProgram::SplToken))?
29    };
30
31    match ata_instruction {
32        AssociatedTokenAccountInstruction::Create => {
33            check_num_associated_token_accounts(&instruction.accounts, 6)?;
34            Ok(ParsedInstructionEnum {
35                instruction_type: "create".to_string(),
36                info: json!({
37                    "source": account_keys[instruction.accounts[0] as usize].to_string(),
38                    "account": account_keys[instruction.accounts[1] as usize].to_string(),
39                    "wallet": account_keys[instruction.accounts[2] as usize].to_string(),
40                    "mint": account_keys[instruction.accounts[3] as usize].to_string(),
41                    "systemProgram": account_keys[instruction.accounts[4] as usize].to_string(),
42                    "tokenProgram": account_keys[instruction.accounts[5] as usize].to_string(),
43                }),
44            })
45        }
46        AssociatedTokenAccountInstruction::CreateIdempotent => {
47            check_num_associated_token_accounts(&instruction.accounts, 6)?;
48            Ok(ParsedInstructionEnum {
49                instruction_type: "createIdempotent".to_string(),
50                info: json!({
51                    "source": account_keys[instruction.accounts[0] as usize].to_string(),
52                    "account": account_keys[instruction.accounts[1] as usize].to_string(),
53                    "wallet": account_keys[instruction.accounts[2] as usize].to_string(),
54                    "mint": account_keys[instruction.accounts[3] as usize].to_string(),
55                    "systemProgram": account_keys[instruction.accounts[4] as usize].to_string(),
56                    "tokenProgram": account_keys[instruction.accounts[5] as usize].to_string(),
57                }),
58            })
59        }
60        AssociatedTokenAccountInstruction::RecoverNested => {
61            check_num_associated_token_accounts(&instruction.accounts, 7)?;
62            Ok(ParsedInstructionEnum {
63                instruction_type: "recoverNested".to_string(),
64                info: json!({
65                    "nestedSource": account_keys[instruction.accounts[0] as usize].to_string(),
66                    "nestedMint": account_keys[instruction.accounts[1] as usize].to_string(),
67                    "destination": account_keys[instruction.accounts[2] as usize].to_string(),
68                    "nestedOwner": account_keys[instruction.accounts[3] as usize].to_string(),
69                    "ownerMint": account_keys[instruction.accounts[4] as usize].to_string(),
70                    "wallet": account_keys[instruction.accounts[5] as usize].to_string(),
71                    "tokenProgram": account_keys[instruction.accounts[6] as usize].to_string(),
72                }),
73            })
74        }
75    }
76}
77
78fn check_num_associated_token_accounts(
79    accounts: &[u8],
80    num: usize,
81) -> Result<(), ParseInstructionError> {
82    check_num_accounts(accounts, num, ParsableProgram::SplAssociatedTokenAccount)
83}
84
85#[cfg(test)]
86mod test {
87    use {
88        super::*,
89        solana_instruction::AccountMeta,
90        solana_message::Message,
91        solana_pubkey::Pubkey,
92        solana_sdk_ids::sysvar,
93        spl_associated_token_account_interface::{
94            address::{get_associated_token_address, get_associated_token_address_with_program_id},
95            instruction::{
96                create_associated_token_account, create_associated_token_account_idempotent,
97                recover_nested,
98            },
99        },
100    };
101
102    #[test]
103    fn test_parse_create_deprecated() {
104        let funder = Pubkey::new_unique();
105        let wallet_address = Pubkey::new_unique();
106        let mint = Pubkey::new_unique();
107        let associated_account_address = get_associated_token_address(&wallet_address, &mint);
108        let token_program_id = spl_token_interface::id();
109        // mimic the deprecated instruction
110        let mut create_ix =
111            create_associated_token_account(&funder, &wallet_address, &mint, &token_program_id);
112        create_ix.data = vec![];
113        create_ix
114            .accounts
115            .push(AccountMeta::new_readonly(sysvar::rent::id(), false));
116        let mut message = Message::new(&[create_ix], None);
117        let compiled_instruction = &mut message.instructions[0];
118        let expected_parsed_ix = ParsedInstructionEnum {
119            instruction_type: "create".to_string(),
120            info: json!({
121                "source": funder.to_string(),
122                "account": associated_account_address.to_string(),
123                "wallet": wallet_address.to_string(),
124                "mint": mint.to_string(),
125                "systemProgram": solana_sdk_ids::system_program::id().to_string(),
126                "tokenProgram": token_program_id.to_string(),
127            }),
128        };
129        assert_eq!(
130            parse_associated_token(
131                compiled_instruction,
132                &AccountKeys::new(&message.account_keys, None)
133            )
134            .unwrap(),
135            expected_parsed_ix,
136        );
137
138        // after popping rent account, parsing should still succeed
139        let rent_account_index = compiled_instruction
140            .accounts
141            .iter()
142            .position(|index| message.account_keys[*index as usize] == sysvar::rent::id())
143            .unwrap();
144        compiled_instruction.accounts.remove(rent_account_index);
145        assert_eq!(
146            parse_associated_token(
147                compiled_instruction,
148                &AccountKeys::new(&message.account_keys, None)
149            )
150            .unwrap(),
151            expected_parsed_ix,
152        );
153
154        // after popping another account, parsing should fail
155        compiled_instruction.accounts.pop();
156        assert!(parse_associated_token(
157            compiled_instruction,
158            &AccountKeys::new(&message.account_keys, None)
159        )
160        .is_err());
161    }
162
163    #[test]
164    fn test_parse_create() {
165        let funder = Pubkey::new_unique();
166        let wallet_address = Pubkey::new_unique();
167        let mint = Pubkey::new_unique();
168        let token_program_id = Pubkey::new_unique();
169        let associated_account_address =
170            get_associated_token_address_with_program_id(&wallet_address, &mint, &token_program_id);
171        let create_ix =
172            create_associated_token_account(&funder, &wallet_address, &mint, &token_program_id);
173        let mut message = Message::new(&[create_ix], None);
174        let compiled_instruction = &mut message.instructions[0];
175        assert_eq!(
176            parse_associated_token(
177                compiled_instruction,
178                &AccountKeys::new(&message.account_keys, None)
179            )
180            .unwrap(),
181            ParsedInstructionEnum {
182                instruction_type: "create".to_string(),
183                info: json!({
184                    "source": funder.to_string(),
185                    "account": associated_account_address.to_string(),
186                    "wallet": wallet_address.to_string(),
187                    "mint": mint.to_string(),
188                    "systemProgram": solana_sdk_ids::system_program::id().to_string(),
189                    "tokenProgram": token_program_id.to_string(),
190                })
191            }
192        );
193        compiled_instruction.accounts.pop();
194        assert!(parse_associated_token(
195            compiled_instruction,
196            &AccountKeys::new(&message.account_keys, None)
197        )
198        .is_err());
199    }
200
201    #[test]
202    fn test_parse_create_idempotent() {
203        let funder = Pubkey::new_unique();
204        let wallet_address = Pubkey::new_unique();
205        let mint = Pubkey::new_unique();
206        let token_program_id = Pubkey::new_unique();
207        let associated_account_address =
208            get_associated_token_address_with_program_id(&wallet_address, &mint, &token_program_id);
209        let create_ix = create_associated_token_account_idempotent(
210            &funder,
211            &wallet_address,
212            &mint,
213            &token_program_id,
214        );
215        let mut message = Message::new(&[create_ix], None);
216        let compiled_instruction = &mut message.instructions[0];
217        assert_eq!(
218            parse_associated_token(
219                compiled_instruction,
220                &AccountKeys::new(&message.account_keys, None)
221            )
222            .unwrap(),
223            ParsedInstructionEnum {
224                instruction_type: "createIdempotent".to_string(),
225                info: json!({
226                    "source": funder.to_string(),
227                    "account": associated_account_address.to_string(),
228                    "wallet": wallet_address.to_string(),
229                    "mint": mint.to_string(),
230                    "systemProgram": solana_sdk_ids::system_program::id().to_string(),
231                    "tokenProgram": token_program_id.to_string(),
232                })
233            }
234        );
235        compiled_instruction.accounts.pop();
236        assert!(parse_associated_token(
237            compiled_instruction,
238            &AccountKeys::new(&message.account_keys, None)
239        )
240        .is_err());
241    }
242
243    #[test]
244    fn test_parse_recover_nested() {
245        let wallet_address = Pubkey::new_unique();
246        let owner_mint = Pubkey::new_unique();
247        let nested_mint = Pubkey::new_unique();
248        let token_program_id = Pubkey::new_unique();
249        let owner_associated_account_address = get_associated_token_address_with_program_id(
250            &wallet_address,
251            &owner_mint,
252            &token_program_id,
253        );
254        let nested_associated_account_address = get_associated_token_address_with_program_id(
255            &owner_associated_account_address,
256            &nested_mint,
257            &token_program_id,
258        );
259        let destination_associated_account_address = get_associated_token_address_with_program_id(
260            &wallet_address,
261            &nested_mint,
262            &token_program_id,
263        );
264        let recover_ix = recover_nested(
265            &wallet_address,
266            &owner_mint,
267            &nested_mint,
268            &token_program_id,
269        );
270        let mut message = Message::new(&[recover_ix], None);
271        let compiled_instruction = &mut message.instructions[0];
272        assert_eq!(
273            parse_associated_token(
274                compiled_instruction,
275                &AccountKeys::new(&message.account_keys, None)
276            )
277            .unwrap(),
278            ParsedInstructionEnum {
279                instruction_type: "recoverNested".to_string(),
280                info: json!({
281                    "nestedSource": nested_associated_account_address.to_string(),
282                    "nestedMint": nested_mint.to_string(),
283                    "destination": destination_associated_account_address.to_string(),
284                    "nestedOwner": owner_associated_account_address.to_string(),
285                    "ownerMint": owner_mint.to_string(),
286                    "wallet": wallet_address.to_string(),
287                    "tokenProgram": token_program_id.to_string(),
288                })
289            }
290        );
291        compiled_instruction.accounts.pop();
292        assert!(parse_associated_token(
293            compiled_instruction,
294            &AccountKeys::new(&message.account_keys, None)
295        )
296        .is_err());
297    }
298}