1pub use spl_transfer_hook_interface::offchain::{AccountDataResult, AccountFetchError};
4use {
5 crate::{
6 extension::{transfer_fee, transfer_hook, StateWithExtensions},
7 state::Mint,
8 },
9 solana_program::{instruction::Instruction, program_error::ProgramError, pubkey::Pubkey},
10 spl_transfer_hook_interface::offchain::add_extra_account_metas_for_execute,
11 std::future::Future,
12};
13
14#[allow(clippy::too_many_arguments)]
37pub async fn create_transfer_checked_instruction_with_extra_metas<F, Fut>(
38 token_program_id: &Pubkey,
39 source_pubkey: &Pubkey,
40 mint_pubkey: &Pubkey,
41 destination_pubkey: &Pubkey,
42 authority_pubkey: &Pubkey,
43 signer_pubkeys: &[&Pubkey],
44 amount: u64,
45 decimals: u8,
46 fetch_account_data_fn: F,
47) -> Result<Instruction, AccountFetchError>
48where
49 F: Fn(Pubkey) -> Fut,
50 Fut: Future<Output = AccountDataResult>,
51{
52 let mut transfer_instruction = crate::instruction::transfer_checked(
53 token_program_id,
54 source_pubkey,
55 mint_pubkey,
56 destination_pubkey,
57 authority_pubkey,
58 signer_pubkeys,
59 amount,
60 decimals,
61 )?;
62
63 add_extra_account_metas(
64 &mut transfer_instruction,
65 source_pubkey,
66 mint_pubkey,
67 destination_pubkey,
68 authority_pubkey,
69 amount,
70 fetch_account_data_fn,
71 )
72 .await?;
73
74 Ok(transfer_instruction)
75}
76
77#[allow(clippy::too_many_arguments)]
101pub async fn create_transfer_checked_with_fee_instruction_with_extra_metas<F, Fut>(
102 token_program_id: &Pubkey,
103 source_pubkey: &Pubkey,
104 mint_pubkey: &Pubkey,
105 destination_pubkey: &Pubkey,
106 authority_pubkey: &Pubkey,
107 signer_pubkeys: &[&Pubkey],
108 amount: u64,
109 decimals: u8,
110 fee: u64,
111 fetch_account_data_fn: F,
112) -> Result<Instruction, AccountFetchError>
113where
114 F: Fn(Pubkey) -> Fut,
115 Fut: Future<Output = AccountDataResult>,
116{
117 let mut transfer_instruction = transfer_fee::instruction::transfer_checked_with_fee(
118 token_program_id,
119 source_pubkey,
120 mint_pubkey,
121 destination_pubkey,
122 authority_pubkey,
123 signer_pubkeys,
124 amount,
125 decimals,
126 fee,
127 )?;
128
129 add_extra_account_metas(
130 &mut transfer_instruction,
131 source_pubkey,
132 mint_pubkey,
133 destination_pubkey,
134 authority_pubkey,
135 amount,
136 fetch_account_data_fn,
137 )
138 .await?;
139
140 Ok(transfer_instruction)
141}
142
143pub async fn add_extra_account_metas<F, Fut>(
172 instruction: &mut Instruction,
173 source_pubkey: &Pubkey,
174 mint_pubkey: &Pubkey,
175 destination_pubkey: &Pubkey,
176 authority_pubkey: &Pubkey,
177 amount: u64,
178 fetch_account_data_fn: F,
179) -> Result<(), AccountFetchError>
180where
181 F: Fn(Pubkey) -> Fut,
182 Fut: Future<Output = AccountDataResult>,
183{
184 let mint_data = fetch_account_data_fn(*mint_pubkey)
185 .await?
186 .ok_or(ProgramError::InvalidAccountData)?;
187 let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
188
189 if let Some(program_id) = transfer_hook::get_program_id(&mint) {
190 add_extra_account_metas_for_execute(
191 instruction,
192 &program_id,
193 source_pubkey,
194 mint_pubkey,
195 destination_pubkey,
196 authority_pubkey,
197 amount,
198 fetch_account_data_fn,
199 )
200 .await?;
201 }
202
203 Ok(())
204}
205
206#[cfg(test)]
207mod tests {
208 use {
209 super::*,
210 crate::extension::{
211 transfer_hook::TransferHook, BaseStateWithExtensionsMut, ExtensionType,
212 StateWithExtensionsMut,
213 },
214 solana_program::{instruction::AccountMeta, program_option::COption},
215 solana_program_test::tokio,
216 spl_pod::optional_keys::OptionalNonZeroPubkey,
217 spl_tlv_account_resolution::{
218 account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList,
219 },
220 spl_transfer_hook_interface::{
221 get_extra_account_metas_address, instruction::ExecuteInstruction,
222 },
223 };
224
225 const DECIMALS: u8 = 0;
226 const MINT_PUBKEY: Pubkey = Pubkey::new_from_array([1u8; 32]);
227 const TRANSFER_HOOK_PROGRAM_ID: Pubkey = Pubkey::new_from_array([2u8; 32]);
228 const EXTRA_META_1: Pubkey = Pubkey::new_from_array([3u8; 32]);
229 const EXTRA_META_2: Pubkey = Pubkey::new_from_array([4u8; 32]);
230
231 async fn mock_fetch_account_data_fn(address: Pubkey) -> AccountDataResult {
233 if address == MINT_PUBKEY {
234 let mint_len =
235 ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::TransferHook])
236 .unwrap();
237 let mut data = vec![0u8; mint_len];
238 let mut mint = StateWithExtensionsMut::<Mint>::unpack_uninitialized(&mut data).unwrap();
239
240 let extension = mint.init_extension::<TransferHook>(true).unwrap();
241 extension.program_id =
242 OptionalNonZeroPubkey::try_from(Some(TRANSFER_HOOK_PROGRAM_ID)).unwrap();
243
244 mint.base.mint_authority = COption::Some(Pubkey::new_unique());
245 mint.base.decimals = DECIMALS;
246 mint.base.is_initialized = true;
247 mint.base.freeze_authority = COption::None;
248 mint.pack_base();
249 mint.init_account_type().unwrap();
250
251 Ok(Some(data))
252 } else if address
253 == get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID)
254 {
255 let extra_metas = vec![
256 ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(),
257 ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(),
258 ExtraAccountMeta::new_with_seeds(
259 &[
260 Seed::AccountKey { index: 0 }, Seed::AccountKey { index: 2 }, Seed::AccountKey { index: 4 }, ],
264 false,
265 true,
266 )
267 .unwrap(),
268 ExtraAccountMeta::new_with_seeds(
269 &[
270 Seed::InstructionData {
271 index: 8,
272 length: 8,
273 }, Seed::AccountKey { index: 2 }, Seed::AccountKey { index: 5 }, Seed::AccountKey { index: 7 }, ],
278 false,
279 true,
280 )
281 .unwrap(),
282 ];
283 let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap();
284 let mut data = vec![0u8; account_size];
285 ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, &extra_metas)?;
286 Ok(Some(data))
287 } else {
288 Ok(None)
289 }
290 }
291
292 #[tokio::test]
293 async fn test_create_transfer_checked_instruction_with_extra_metas() {
294 let source = Pubkey::new_unique();
295 let destination = Pubkey::new_unique();
296 let authority = Pubkey::new_unique();
297 let amount = 100u64;
298
299 let validate_state_pubkey =
300 get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID);
301 let extra_meta_3_pubkey = Pubkey::find_program_address(
302 &[
303 source.as_ref(),
304 destination.as_ref(),
305 validate_state_pubkey.as_ref(),
306 ],
307 &TRANSFER_HOOK_PROGRAM_ID,
308 )
309 .0;
310 let extra_meta_4_pubkey = Pubkey::find_program_address(
311 &[
312 amount.to_le_bytes().as_ref(),
313 destination.as_ref(),
314 EXTRA_META_1.as_ref(),
315 extra_meta_3_pubkey.as_ref(),
316 ],
317 &TRANSFER_HOOK_PROGRAM_ID,
318 )
319 .0;
320
321 let instruction = create_transfer_checked_instruction_with_extra_metas(
322 &crate::id(),
323 &source,
324 &MINT_PUBKEY,
325 &destination,
326 &authority,
327 &[],
328 amount,
329 DECIMALS,
330 mock_fetch_account_data_fn,
331 )
332 .await
333 .unwrap();
334
335 let check_metas = [
336 AccountMeta::new(source, false),
337 AccountMeta::new_readonly(MINT_PUBKEY, false),
338 AccountMeta::new(destination, false),
339 AccountMeta::new_readonly(authority, true),
340 AccountMeta::new_readonly(EXTRA_META_1, true),
341 AccountMeta::new_readonly(EXTRA_META_2, true),
342 AccountMeta::new(extra_meta_3_pubkey, false),
343 AccountMeta::new(extra_meta_4_pubkey, false),
344 AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false),
345 AccountMeta::new_readonly(validate_state_pubkey, false),
346 ];
347
348 assert_eq!(instruction.accounts, check_metas);
349
350 let signer_1 = Pubkey::new_unique();
352 let signer_2 = Pubkey::new_unique();
353 let signer_3 = Pubkey::new_unique();
354
355 let instruction = create_transfer_checked_instruction_with_extra_metas(
356 &crate::id(),
357 &source,
358 &MINT_PUBKEY,
359 &destination,
360 &authority,
361 &[&signer_1, &signer_2, &signer_3],
362 amount,
363 DECIMALS,
364 mock_fetch_account_data_fn,
365 )
366 .await
367 .unwrap();
368
369 let check_metas = [
370 AccountMeta::new(source, false),
371 AccountMeta::new_readonly(MINT_PUBKEY, false),
372 AccountMeta::new(destination, false),
373 AccountMeta::new_readonly(authority, false), AccountMeta::new_readonly(signer_1, true),
375 AccountMeta::new_readonly(signer_2, true),
376 AccountMeta::new_readonly(signer_3, true),
377 AccountMeta::new_readonly(EXTRA_META_1, true),
378 AccountMeta::new_readonly(EXTRA_META_2, true),
379 AccountMeta::new(extra_meta_3_pubkey, false),
380 AccountMeta::new(extra_meta_4_pubkey, false),
381 AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false),
382 AccountMeta::new_readonly(validate_state_pubkey, false),
383 ];
384
385 assert_eq!(instruction.accounts, check_metas);
386 }
387
388 #[tokio::test]
389 async fn test_create_transfer_checked_with_fee_instruction_with_extra_metas() {
390 let source = Pubkey::new_unique();
391 let destination = Pubkey::new_unique();
392 let authority = Pubkey::new_unique();
393 let amount = 100u64;
394 let fee = 1u64;
395
396 let validate_state_pubkey =
397 get_extra_account_metas_address(&MINT_PUBKEY, &TRANSFER_HOOK_PROGRAM_ID);
398 let extra_meta_3_pubkey = Pubkey::find_program_address(
399 &[
400 source.as_ref(),
401 destination.as_ref(),
402 validate_state_pubkey.as_ref(),
403 ],
404 &TRANSFER_HOOK_PROGRAM_ID,
405 )
406 .0;
407 let extra_meta_4_pubkey = Pubkey::find_program_address(
408 &[
409 amount.to_le_bytes().as_ref(),
410 destination.as_ref(),
411 EXTRA_META_1.as_ref(),
412 extra_meta_3_pubkey.as_ref(),
413 ],
414 &TRANSFER_HOOK_PROGRAM_ID,
415 )
416 .0;
417
418 let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas(
419 &crate::id(),
420 &source,
421 &MINT_PUBKEY,
422 &destination,
423 &authority,
424 &[],
425 amount,
426 DECIMALS,
427 fee,
428 mock_fetch_account_data_fn,
429 )
430 .await
431 .unwrap();
432
433 let check_metas = [
434 AccountMeta::new(source, false),
435 AccountMeta::new_readonly(MINT_PUBKEY, false),
436 AccountMeta::new(destination, false),
437 AccountMeta::new_readonly(authority, true),
438 AccountMeta::new_readonly(EXTRA_META_1, true),
439 AccountMeta::new_readonly(EXTRA_META_2, true),
440 AccountMeta::new(extra_meta_3_pubkey, false),
441 AccountMeta::new(extra_meta_4_pubkey, false),
442 AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false),
443 AccountMeta::new_readonly(validate_state_pubkey, false),
444 ];
445
446 assert_eq!(instruction.accounts, check_metas);
447
448 let signer_1 = Pubkey::new_unique();
450 let signer_2 = Pubkey::new_unique();
451 let signer_3 = Pubkey::new_unique();
452
453 let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas(
454 &crate::id(),
455 &source,
456 &MINT_PUBKEY,
457 &destination,
458 &authority,
459 &[&signer_1, &signer_2, &signer_3],
460 amount,
461 DECIMALS,
462 fee,
463 mock_fetch_account_data_fn,
464 )
465 .await
466 .unwrap();
467
468 let check_metas = [
469 AccountMeta::new(source, false),
470 AccountMeta::new_readonly(MINT_PUBKEY, false),
471 AccountMeta::new(destination, false),
472 AccountMeta::new_readonly(authority, false), AccountMeta::new_readonly(signer_1, true),
474 AccountMeta::new_readonly(signer_2, true),
475 AccountMeta::new_readonly(signer_3, true),
476 AccountMeta::new_readonly(EXTRA_META_1, true),
477 AccountMeta::new_readonly(EXTRA_META_2, true),
478 AccountMeta::new(extra_meta_3_pubkey, false),
479 AccountMeta::new(extra_meta_4_pubkey, false),
480 AccountMeta::new_readonly(TRANSFER_HOOK_PROGRAM_ID, false),
481 AccountMeta::new_readonly(validate_state_pubkey, false),
482 ];
483
484 assert_eq!(instruction.accounts, check_metas);
485 }
486}