spl_transfer_hook_interface/
onchain.rs

1//! On-chain program invoke helper to perform on-chain `execute` with correct
2//! accounts
3
4use {
5    crate::{error::TransferHookError, get_extra_account_metas_address, instruction},
6    miraland_program::{
7        account_info::AccountInfo,
8        entrypoint::ProgramResult,
9        instruction::{AccountMeta, Instruction},
10        program::invoke,
11        pubkey::Pubkey,
12    },
13    spl_tlv_account_resolution::state::ExtraAccountMetaList,
14};
15/// Helper to CPI into a transfer-hook program on-chain, looking through the
16/// additional account infos to create the proper instruction
17pub fn invoke_execute<'a>(
18    program_id: &Pubkey,
19    source_info: AccountInfo<'a>,
20    mint_info: AccountInfo<'a>,
21    destination_info: AccountInfo<'a>,
22    authority_info: AccountInfo<'a>,
23    additional_accounts: &[AccountInfo<'a>],
24    amount: u64,
25) -> ProgramResult {
26    let validation_pubkey = get_extra_account_metas_address(mint_info.key, program_id);
27    let validation_info = additional_accounts
28        .iter()
29        .find(|&x| *x.key == validation_pubkey)
30        .ok_or(TransferHookError::IncorrectAccount)?;
31    let mut cpi_instruction = instruction::execute(
32        program_id,
33        source_info.key,
34        mint_info.key,
35        destination_info.key,
36        authority_info.key,
37        &validation_pubkey,
38        amount,
39    );
40
41    let mut cpi_account_infos = vec![
42        source_info,
43        mint_info,
44        destination_info,
45        authority_info,
46        validation_info.clone(),
47    ];
48    ExtraAccountMetaList::add_to_cpi_instruction::<instruction::ExecuteInstruction>(
49        &mut cpi_instruction,
50        &mut cpi_account_infos,
51        &validation_info.try_borrow_data()?,
52        additional_accounts,
53    )?;
54    invoke(&cpi_instruction, &cpi_account_infos)
55}
56
57/// Helper to add accounts required for an `ExecuteInstruction` on-chain,
58/// looking through the additional account infos to add the proper accounts.
59///
60/// Note this helper is designed to add the extra accounts that will be
61/// required for a CPI to a transfer hook program. However, the instruction
62/// being provided to this helper is for the program that will CPI to the
63/// transfer hook program. Because of this, we must resolve the extra accounts
64/// for the `ExecuteInstruction` CPI, then add those extra resolved accounts to
65/// the provided instruction.
66#[allow(clippy::too_many_arguments)]
67pub fn add_extra_accounts_for_execute_cpi<'a>(
68    cpi_instruction: &mut Instruction,
69    cpi_account_infos: &mut Vec<AccountInfo<'a>>,
70    program_id: &Pubkey,
71    source_info: AccountInfo<'a>,
72    mint_info: AccountInfo<'a>,
73    destination_info: AccountInfo<'a>,
74    authority_info: AccountInfo<'a>,
75    amount: u64,
76    additional_accounts: &[AccountInfo<'a>],
77) -> ProgramResult {
78    let validate_state_pubkey = get_extra_account_metas_address(mint_info.key, program_id);
79    let validate_state_info = additional_accounts
80        .iter()
81        .find(|&x| *x.key == validate_state_pubkey)
82        .ok_or(TransferHookError::IncorrectAccount)?;
83
84    let program_info = additional_accounts
85        .iter()
86        .find(|&x| x.key == program_id)
87        .ok_or(TransferHookError::IncorrectAccount)?;
88
89    let mut execute_instruction = instruction::execute(
90        program_id,
91        source_info.key,
92        mint_info.key,
93        destination_info.key,
94        authority_info.key,
95        &validate_state_pubkey,
96        amount,
97    );
98    let mut execute_account_infos = vec![
99        source_info,
100        mint_info,
101        destination_info,
102        authority_info,
103        validate_state_info.clone(),
104    ];
105
106    ExtraAccountMetaList::add_to_cpi_instruction::<instruction::ExecuteInstruction>(
107        &mut execute_instruction,
108        &mut execute_account_infos,
109        &validate_state_info.try_borrow_data()?,
110        additional_accounts,
111    )?;
112
113    // Add only the extra accounts resolved from the validation state
114    cpi_instruction
115        .accounts
116        .extend_from_slice(&execute_instruction.accounts[5..]);
117    cpi_account_infos.extend_from_slice(&execute_account_infos[5..]);
118
119    // Add the program id and validation state account
120    cpi_instruction
121        .accounts
122        .push(AccountMeta::new_readonly(*program_id, false));
123    cpi_instruction
124        .accounts
125        .push(AccountMeta::new_readonly(validate_state_pubkey, false));
126    cpi_account_infos.push(program_info.clone());
127    cpi_account_infos.push(validate_state_info.clone());
128
129    Ok(())
130}
131
132#[cfg(test)]
133mod tests {
134    use {
135        super::*,
136        crate::instruction::ExecuteInstruction,
137        miraland_program::{bpf_loader_upgradeable, system_program},
138        spl_tlv_account_resolution::{
139            account::ExtraAccountMeta, error::AccountResolutionError, seeds::Seed,
140        },
141    };
142
143    const EXTRA_META_1: Pubkey = Pubkey::new_from_array([2u8; 32]);
144    const EXTRA_META_2: Pubkey = Pubkey::new_from_array([3u8; 32]);
145
146    fn setup_validation_data() -> Vec<u8> {
147        let extra_metas = vec![
148            ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(),
149            ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(),
150            ExtraAccountMeta::new_with_seeds(
151                &[
152                    Seed::AccountKey { index: 0 }, // source
153                    Seed::AccountKey { index: 2 }, // destination
154                    Seed::AccountKey { index: 4 }, // validation state
155                ],
156                false,
157                true,
158            )
159            .unwrap(),
160            ExtraAccountMeta::new_with_seeds(
161                &[
162                    Seed::InstructionData {
163                        index: 8,
164                        length: 8,
165                    }, // amount
166                    Seed::AccountKey { index: 2 }, // destination
167                    Seed::AccountKey { index: 5 }, // extra meta 1
168                    Seed::AccountKey { index: 7 }, // extra meta 3 (PDA)
169                ],
170                false,
171                true,
172            )
173            .unwrap(),
174        ];
175        let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap();
176        let mut data = vec![0u8; account_size];
177        ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, &extra_metas).unwrap();
178        data
179    }
180
181    #[test]
182    fn test_add_extra_accounts_for_execute_cpi() {
183        let spl_token_2022_program_id = Pubkey::new_unique(); // Mock
184        let transfer_hook_program_id = Pubkey::new_unique();
185
186        let amount = 100u64;
187
188        let source_pubkey = Pubkey::new_unique();
189        let mut source_data = vec![0; 165]; // Mock
190        let mut source_lamports = 0; // Mock
191        let source_account_info = AccountInfo::new(
192            &source_pubkey,
193            false,
194            true,
195            &mut source_lamports,
196            &mut source_data,
197            &spl_token_2022_program_id,
198            false,
199            0,
200        );
201
202        let mint_pubkey = Pubkey::new_unique();
203        let mut mint_data = vec![0; 165]; // Mock
204        let mut mint_lamports = 0; // Mock
205        let mint_account_info = AccountInfo::new(
206            &mint_pubkey,
207            false,
208            true,
209            &mut mint_lamports,
210            &mut mint_data,
211            &spl_token_2022_program_id,
212            false,
213            0,
214        );
215
216        let destination_pubkey = Pubkey::new_unique();
217        let mut destination_data = vec![0; 165]; // Mock
218        let mut destination_lamports = 0; // Mock
219        let destination_account_info = AccountInfo::new(
220            &destination_pubkey,
221            false,
222            true,
223            &mut destination_lamports,
224            &mut destination_data,
225            &spl_token_2022_program_id,
226            false,
227            0,
228        );
229
230        let authority_pubkey = Pubkey::new_unique();
231        let mut authority_data = vec![]; // Mock
232        let mut authority_lamports = 0; // Mock
233        let authority_account_info = AccountInfo::new(
234            &authority_pubkey,
235            false,
236            true,
237            &mut authority_lamports,
238            &mut authority_data,
239            &system_program::ID,
240            false,
241            0,
242        );
243
244        let validate_state_pubkey =
245            get_extra_account_metas_address(&mint_pubkey, &transfer_hook_program_id);
246
247        let extra_meta_1_pubkey = EXTRA_META_1;
248        let mut extra_meta_1_data = vec![]; // Mock
249        let mut extra_meta_1_lamports = 0; // Mock
250        let extra_meta_1_account_info = AccountInfo::new(
251            &extra_meta_1_pubkey,
252            true,
253            false,
254            &mut extra_meta_1_lamports,
255            &mut extra_meta_1_data,
256            &system_program::ID,
257            false,
258            0,
259        );
260
261        let extra_meta_2_pubkey = EXTRA_META_2;
262        let mut extra_meta_2_data = vec![]; // Mock
263        let mut extra_meta_2_lamports = 0; // Mock
264        let extra_meta_2_account_info = AccountInfo::new(
265            &extra_meta_2_pubkey,
266            true,
267            false,
268            &mut extra_meta_2_lamports,
269            &mut extra_meta_2_data,
270            &system_program::ID,
271            false,
272            0,
273        );
274
275        let extra_meta_3_pubkey = Pubkey::find_program_address(
276            &[
277                &source_pubkey.to_bytes(),
278                &destination_pubkey.to_bytes(),
279                &validate_state_pubkey.to_bytes(),
280            ],
281            &transfer_hook_program_id,
282        )
283        .0;
284        let mut extra_meta_3_data = vec![]; // Mock
285        let mut extra_meta_3_lamports = 0; // Mock
286        let extra_meta_3_account_info = AccountInfo::new(
287            &extra_meta_3_pubkey,
288            false,
289            true,
290            &mut extra_meta_3_lamports,
291            &mut extra_meta_3_data,
292            &transfer_hook_program_id,
293            false,
294            0,
295        );
296
297        let extra_meta_4_pubkey = Pubkey::find_program_address(
298            &[
299                &amount.to_le_bytes(),
300                &destination_pubkey.to_bytes(),
301                &extra_meta_1_pubkey.to_bytes(),
302                &extra_meta_3_pubkey.to_bytes(),
303            ],
304            &transfer_hook_program_id,
305        )
306        .0;
307        let mut extra_meta_4_data = vec![]; // Mock
308        let mut extra_meta_4_lamports = 0; // Mock
309        let extra_meta_4_account_info = AccountInfo::new(
310            &extra_meta_4_pubkey,
311            false,
312            true,
313            &mut extra_meta_4_lamports,
314            &mut extra_meta_4_data,
315            &transfer_hook_program_id,
316            false,
317            0,
318        );
319
320        let mut validate_state_data = setup_validation_data();
321        let mut validate_state_lamports = 0; // Mock
322        let validate_state_account_info = AccountInfo::new(
323            &validate_state_pubkey,
324            false,
325            true,
326            &mut validate_state_lamports,
327            &mut validate_state_data,
328            &transfer_hook_program_id,
329            false,
330            0,
331        );
332
333        let mut transfer_hook_program_data = vec![]; // Mock
334        let mut transfer_hook_program_lamports = 0; // Mock
335        let transfer_hook_program_account_info = AccountInfo::new(
336            &transfer_hook_program_id,
337            false,
338            true,
339            &mut transfer_hook_program_lamports,
340            &mut transfer_hook_program_data,
341            &bpf_loader_upgradeable::ID,
342            false,
343            0,
344        );
345
346        let mut cpi_instruction = Instruction::new_with_bytes(
347            spl_token_2022_program_id,
348            &[],
349            vec![
350                AccountMeta::new(source_pubkey, false),
351                AccountMeta::new_readonly(mint_pubkey, false),
352                AccountMeta::new(destination_pubkey, false),
353                AccountMeta::new_readonly(authority_pubkey, true),
354            ],
355        );
356        let mut cpi_account_infos = vec![
357            source_account_info.clone(),
358            mint_account_info.clone(),
359            destination_account_info.clone(),
360            authority_account_info.clone(),
361        ];
362        let additional_account_infos = vec![
363            extra_meta_1_account_info.clone(),
364            extra_meta_2_account_info.clone(),
365            extra_meta_3_account_info.clone(),
366            extra_meta_4_account_info.clone(),
367            transfer_hook_program_account_info.clone(),
368            validate_state_account_info.clone(),
369        ];
370
371        // Fail missing validation info from additional account infos
372        let additional_account_infos_missing_infos = vec![
373            extra_meta_1_account_info.clone(),
374            extra_meta_2_account_info.clone(),
375            extra_meta_3_account_info.clone(),
376            extra_meta_4_account_info.clone(),
377            // validate state missing
378            transfer_hook_program_account_info.clone(),
379        ];
380        assert_eq!(
381            add_extra_accounts_for_execute_cpi(
382                &mut cpi_instruction,
383                &mut cpi_account_infos,
384                &transfer_hook_program_id,
385                source_account_info.clone(),
386                mint_account_info.clone(),
387                destination_account_info.clone(),
388                authority_account_info.clone(),
389                amount,
390                &additional_account_infos_missing_infos, // Missing account info
391            )
392            .unwrap_err(),
393            TransferHookError::IncorrectAccount.into()
394        );
395
396        // Fail missing program info from additional account infos
397        let additional_account_infos_missing_infos = vec![
398            extra_meta_1_account_info.clone(),
399            extra_meta_2_account_info.clone(),
400            extra_meta_3_account_info.clone(),
401            extra_meta_4_account_info.clone(),
402            validate_state_account_info.clone(),
403            // transfer hook program missing
404        ];
405        assert_eq!(
406            add_extra_accounts_for_execute_cpi(
407                &mut cpi_instruction,
408                &mut cpi_account_infos,
409                &transfer_hook_program_id,
410                source_account_info.clone(),
411                mint_account_info.clone(),
412                destination_account_info.clone(),
413                authority_account_info.clone(),
414                amount,
415                &additional_account_infos_missing_infos, // Missing account info
416            )
417            .unwrap_err(),
418            TransferHookError::IncorrectAccount.into()
419        );
420
421        // Fail missing extra meta info from additional account infos
422        let additional_account_infos_missing_infos = vec![
423            extra_meta_1_account_info.clone(),
424            extra_meta_2_account_info.clone(),
425            // extra meta 3 missing
426            extra_meta_4_account_info.clone(),
427            validate_state_account_info.clone(),
428            transfer_hook_program_account_info.clone(),
429        ];
430        assert_eq!(
431            add_extra_accounts_for_execute_cpi(
432                &mut cpi_instruction,
433                &mut cpi_account_infos,
434                &transfer_hook_program_id,
435                source_account_info.clone(),
436                mint_account_info.clone(),
437                destination_account_info.clone(),
438                authority_account_info.clone(),
439                amount,
440                &additional_account_infos_missing_infos, // Missing account info
441            )
442            .unwrap_err(),
443            AccountResolutionError::IncorrectAccount.into() // Note the error
444        );
445
446        // Success
447        add_extra_accounts_for_execute_cpi(
448            &mut cpi_instruction,
449            &mut cpi_account_infos,
450            &transfer_hook_program_id,
451            source_account_info.clone(),
452            mint_account_info.clone(),
453            destination_account_info.clone(),
454            authority_account_info.clone(),
455            amount,
456            &additional_account_infos,
457        )
458        .unwrap();
459
460        let check_metas = [
461            AccountMeta::new(source_pubkey, false),
462            AccountMeta::new_readonly(mint_pubkey, false),
463            AccountMeta::new(destination_pubkey, false),
464            AccountMeta::new_readonly(authority_pubkey, true),
465            AccountMeta::new_readonly(EXTRA_META_1, true),
466            AccountMeta::new_readonly(EXTRA_META_2, true),
467            AccountMeta::new(extra_meta_3_pubkey, false),
468            AccountMeta::new(extra_meta_4_pubkey, false),
469            AccountMeta::new_readonly(transfer_hook_program_id, false),
470            AccountMeta::new_readonly(validate_state_pubkey, false),
471        ];
472
473        let check_account_infos = vec![
474            source_account_info,
475            mint_account_info,
476            destination_account_info,
477            authority_account_info,
478            extra_meta_1_account_info,
479            extra_meta_2_account_info,
480            extra_meta_3_account_info,
481            extra_meta_4_account_info,
482            transfer_hook_program_account_info,
483            validate_state_account_info,
484        ];
485
486        assert_eq!(cpi_instruction.accounts, check_metas);
487        for (a, b) in std::iter::zip(cpi_account_infos, check_account_infos) {
488            assert_eq!(a.key, b.key);
489            assert_eq!(a.is_signer, b.is_signer);
490            assert_eq!(a.is_writable, b.is_writable);
491        }
492    }
493}