spl_transfer_hook_interface/
offchain.rs

1//! Offchain helper for fetching required accounts to build instructions
2
3pub use spl_tlv_account_resolution::state::{AccountDataResult, AccountFetchError};
4use {
5    crate::{
6        error::TransferHookError,
7        get_extra_account_metas_address,
8        instruction::{execute, ExecuteInstruction},
9    },
10    solana_instruction::{AccountMeta, Instruction},
11    solana_program_error::ProgramError,
12    solana_pubkey::Pubkey,
13    spl_tlv_account_resolution::state::ExtraAccountMetaList,
14    std::future::Future,
15};
16
17/// Offchain helper to get all additional required account metas for an execute
18/// instruction, based on a validation state account.
19///
20/// The instruction being provided to this function must contain at least the
21/// same account keys as the ones being provided, in order. Specifically:
22/// 1. source
23/// 2. mint
24/// 3. destination
25/// 4. authority
26///
27/// The `program_id` should be the program ID of the program that the
28/// created `ExecuteInstruction` is for.
29///
30/// To be client-agnostic and to avoid pulling in the full solana-sdk, this
31/// simply takes a function that will return its data as `Future<Vec<u8>>` for
32/// the given address. Can be called in the following way:
33///
34/// ```rust,ignore
35/// add_extra_account_metas_for_execute(
36///     &mut instruction,
37///     &program_id,
38///     &source,
39///     &mint,
40///     &destination,
41///     &authority,
42///     amount,
43///     |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)),
44/// )
45/// .await?;
46/// ```
47#[allow(clippy::too_many_arguments)]
48pub async fn add_extra_account_metas_for_execute<F, Fut>(
49    instruction: &mut Instruction,
50    program_id: &Pubkey,
51    source_pubkey: &Pubkey,
52    mint_pubkey: &Pubkey,
53    destination_pubkey: &Pubkey,
54    authority_pubkey: &Pubkey,
55    amount: u64,
56    fetch_account_data_fn: F,
57) -> Result<(), AccountFetchError>
58where
59    F: Fn(Pubkey) -> Fut,
60    Fut: Future<Output = AccountDataResult>,
61{
62    let validate_state_pubkey = get_extra_account_metas_address(mint_pubkey, program_id);
63    let validate_state_data = fetch_account_data_fn(validate_state_pubkey)
64        .await?
65        .ok_or(ProgramError::InvalidAccountData)?;
66
67    // Check to make sure the provided keys are in the instruction
68    if [
69        source_pubkey,
70        mint_pubkey,
71        destination_pubkey,
72        authority_pubkey,
73    ]
74    .iter()
75    .any(|&key| !instruction.accounts.iter().any(|meta| meta.pubkey == *key))
76    {
77        Err(TransferHookError::IncorrectAccount)?;
78    }
79
80    let mut execute_instruction = execute(
81        program_id,
82        source_pubkey,
83        mint_pubkey,
84        destination_pubkey,
85        authority_pubkey,
86        amount,
87    );
88    execute_instruction
89        .accounts
90        .push(AccountMeta::new_readonly(validate_state_pubkey, false));
91
92    ExtraAccountMetaList::add_to_instruction::<ExecuteInstruction, _, _>(
93        &mut execute_instruction,
94        fetch_account_data_fn,
95        &validate_state_data,
96    )
97    .await?;
98
99    // Add only the extra accounts resolved from the validation state
100    instruction
101        .accounts
102        .extend_from_slice(&execute_instruction.accounts[5..]);
103
104    // Add the program id and validation state account
105    instruction
106        .accounts
107        .push(AccountMeta::new_readonly(*program_id, false));
108    instruction
109        .accounts
110        .push(AccountMeta::new_readonly(validate_state_pubkey, false));
111
112    Ok(())
113}
114
115#[cfg(test)]
116mod tests {
117    use {
118        super::*,
119        spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed},
120        tokio,
121    };
122
123    const PROGRAM_ID: Pubkey = Pubkey::new_from_array([1u8; 32]);
124    const EXTRA_META_1: Pubkey = Pubkey::new_from_array([2u8; 32]);
125    const EXTRA_META_2: Pubkey = Pubkey::new_from_array([3u8; 32]);
126
127    // Mock to return the validation state account data
128    async fn mock_fetch_account_data_fn(_address: Pubkey) -> AccountDataResult {
129        let extra_metas = vec![
130            ExtraAccountMeta::new_with_pubkey(&EXTRA_META_1, true, false).unwrap(),
131            ExtraAccountMeta::new_with_pubkey(&EXTRA_META_2, true, false).unwrap(),
132            ExtraAccountMeta::new_with_seeds(
133                &[
134                    Seed::AccountKey { index: 0 }, // source
135                    Seed::AccountKey { index: 2 }, // destination
136                    Seed::AccountKey { index: 4 }, // validation state
137                ],
138                false,
139                true,
140            )
141            .unwrap(),
142            ExtraAccountMeta::new_with_seeds(
143                &[
144                    Seed::InstructionData {
145                        index: 8,
146                        length: 8,
147                    }, // amount
148                    Seed::AccountKey { index: 2 }, // destination
149                    Seed::AccountKey { index: 5 }, // extra meta 1
150                    Seed::AccountKey { index: 7 }, // extra meta 3 (PDA)
151                ],
152                false,
153                true,
154            )
155            .unwrap(),
156        ];
157        let account_size = ExtraAccountMetaList::size_of(extra_metas.len()).unwrap();
158        let mut data = vec![0u8; account_size];
159        ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, &extra_metas)?;
160        Ok(Some(data))
161    }
162
163    #[tokio::test]
164    async fn test_add_extra_account_metas_for_execute() {
165        let source = Pubkey::new_unique();
166        let mint = Pubkey::new_unique();
167        let destination = Pubkey::new_unique();
168        let authority = Pubkey::new_unique();
169        let amount = 100u64;
170
171        let validate_state_pubkey = get_extra_account_metas_address(&mint, &PROGRAM_ID);
172        let extra_meta_3_pubkey = Pubkey::find_program_address(
173            &[
174                source.as_ref(),
175                destination.as_ref(),
176                validate_state_pubkey.as_ref(),
177            ],
178            &PROGRAM_ID,
179        )
180        .0;
181        let extra_meta_4_pubkey = Pubkey::find_program_address(
182            &[
183                amount.to_le_bytes().as_ref(),
184                destination.as_ref(),
185                EXTRA_META_1.as_ref(),
186                extra_meta_3_pubkey.as_ref(),
187            ],
188            &PROGRAM_ID,
189        )
190        .0;
191
192        // Fail missing key
193        let mut instruction = Instruction::new_with_bytes(
194            PROGRAM_ID,
195            &[],
196            vec![
197                // source missing
198                AccountMeta::new_readonly(mint, false),
199                AccountMeta::new(destination, false),
200                AccountMeta::new_readonly(authority, true),
201            ],
202        );
203        assert_eq!(
204            add_extra_account_metas_for_execute(
205                &mut instruction,
206                &PROGRAM_ID,
207                &source,
208                &mint,
209                &destination,
210                &authority,
211                amount,
212                mock_fetch_account_data_fn,
213            )
214            .await
215            .unwrap_err()
216            .downcast::<TransferHookError>()
217            .unwrap(),
218            Box::new(TransferHookError::IncorrectAccount)
219        );
220
221        // Success
222        let mut instruction = Instruction::new_with_bytes(
223            PROGRAM_ID,
224            &[],
225            vec![
226                AccountMeta::new(source, false),
227                AccountMeta::new_readonly(mint, false),
228                AccountMeta::new(destination, false),
229                AccountMeta::new_readonly(authority, true),
230            ],
231        );
232        add_extra_account_metas_for_execute(
233            &mut instruction,
234            &PROGRAM_ID,
235            &source,
236            &mint,
237            &destination,
238            &authority,
239            amount,
240            mock_fetch_account_data_fn,
241        )
242        .await
243        .unwrap();
244
245        let check_metas = [
246            AccountMeta::new(source, false),
247            AccountMeta::new_readonly(mint, false),
248            AccountMeta::new(destination, false),
249            AccountMeta::new_readonly(authority, true),
250            AccountMeta::new_readonly(EXTRA_META_1, true),
251            AccountMeta::new_readonly(EXTRA_META_2, true),
252            AccountMeta::new(extra_meta_3_pubkey, false),
253            AccountMeta::new(extra_meta_4_pubkey, false),
254            AccountMeta::new_readonly(PROGRAM_ID, false),
255            AccountMeta::new_readonly(validate_state_pubkey, false),
256        ];
257
258        assert_eq!(instruction.accounts, check_metas);
259    }
260}