solana_transaction_status/
parse_associated_token.rs1use {
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 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 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 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 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}