spl_token_2022/
offchain.rs

1//! Offchain helper for fetching required accounts to build instructions
2
3pub 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/// Offchain helper to create a `TransferChecked` instruction with all
15/// additional required account metas for a transfer, including the ones
16/// required by the transfer hook.
17///
18/// To be client-agnostic and to avoid pulling in the full solana-sdk, this
19/// simply takes a function that will return its data as `Future<Vec<u8>>` for
20/// the given address. Can be called in the following way:
21///
22/// ```rust,ignore
23/// let instruction = create_transfer_checked_instruction_with_extra_metas(
24///     &spl_token_2022::id(),
25///     &source,
26///     &mint,
27///     &destination,
28///     &authority,
29///     &[],
30///     amount,
31///     decimals,
32///     |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)),
33/// )
34/// .await?
35/// ```
36#[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/// Offchain helper to create a `TransferCheckedWithFee` instruction with all
78/// additional required account metas for a transfer, including the ones
79/// required by the transfer hook.
80///
81/// To be client-agnostic and to avoid pulling in the full solana-sdk, this
82/// simply takes a function that will return its data as `Future<Vec<u8>>` for
83/// the given address. Can be called in the following way:
84///
85/// ```rust,ignore
86/// let instruction = create_transfer_checked_with_fee_instruction_with_extra_metas(
87///     &spl_token_2022::id(),
88///     &source,
89///     &mint,
90///     &destination,
91///     &authority,
92///     &[],
93///     amount,
94///     decimals,
95///     fee,
96///     |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)),
97/// )
98/// .await?
99/// ```
100#[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
143/// Offchain helper to add required account metas to an instruction, including
144/// the ones required by the transfer hook.
145///
146/// To be client-agnostic and to avoid pulling in the full solana-sdk, this
147/// simply takes a function that will return its data as `Future<Vec<u8>>` for
148/// the given address. Can be called in the following way:
149///
150/// ```rust,ignore
151/// let mut transfer_instruction = spl_token_2022::instruction::transfer_checked(
152///     &spl_token_2022::id(),
153///     source_pubkey,
154///     mint_pubkey,
155///     destination_pubkey,
156///     authority_pubkey,
157///     signer_pubkeys,
158///     amount,
159///     decimals,
160/// )?;
161/// add_extra_account_metas(
162///     &mut transfer_instruction,
163///     source_pubkey,
164///     mint_pubkey,
165///     destination_pubkey,
166///     authority_pubkey,
167///     amount,
168///     fetch_account_data_fn,
169/// ).await?;
170/// ```
171pub 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    // Mock to return the mint data or the validation state account data
232    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 }, // source
261                        Seed::AccountKey { index: 2 }, // destination
262                        Seed::AccountKey { index: 4 }, // validation state
263                    ],
264                    false,
265                    true,
266                )
267                .unwrap(),
268                ExtraAccountMeta::new_with_seeds(
269                    &[
270                        Seed::InstructionData {
271                            index: 8,
272                            length: 8,
273                        }, // amount
274                        Seed::AccountKey { index: 2 }, // destination
275                        Seed::AccountKey { index: 5 }, // extra meta 1
276                        Seed::AccountKey { index: 7 }, // extra meta 3 (PDA)
277                    ],
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        // With additional signers
351        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), // False because of additional signers
374            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        // With additional signers
449        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), // False because of additional signers
473            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}