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    miraland_program::{
11        instruction::{AccountMeta, Instruction},
12        program_error::ProgramError,
13        pubkey::Pubkey,
14    },
15    spl_tlv_account_resolution::state::ExtraAccountMetaList,
16    std::future::Future,
17};
18
19/// Offchain helper to get all additional required account metas for an execute
20/// instruction, based on a validation state account.
21///
22/// The instruction being provided to this function must contain at least the
23/// same account keys as the ones being provided, in order. Specifically:
24/// 1. source
25/// 2. mint
26/// 3. destination
27/// 4. authority
28///
29/// The `program_id` should be the program ID of the program that the
30/// created `ExecuteInstruction` is for.
31///
32/// To be client-agnostic and to avoid pulling in the full miraland-sdk, this
33/// simply takes a function that will return its data as `Future<Vec<u8>>` for
34/// the given address. Can be called in the following way:
35///
36/// ```rust,ignore
37/// add_extra_account_metas_for_execute(
38///     &mut instruction,
39///     &program_id,
40///     &source,
41///     &mint,
42///     &destination,
43///     &authority,
44///     amount,
45///     |address| self.client.get_account(&address).map_ok(|opt| opt.map(|acc| acc.data)),
46/// )
47/// .await?;
48/// ```
49#[allow(clippy::too_many_arguments)]
50pub async fn add_extra_account_metas_for_execute<F, Fut>(
51    instruction: &mut Instruction,
52    program_id: &Pubkey,
53    source_pubkey: &Pubkey,
54    mint_pubkey: &Pubkey,
55    destination_pubkey: &Pubkey,
56    authority_pubkey: &Pubkey,
57    amount: u64,
58    fetch_account_data_fn: F,
59) -> Result<(), AccountFetchError>
60where
61    F: Fn(Pubkey) -> Fut,
62    Fut: Future<Output = AccountDataResult>,
63{
64    let validate_state_pubkey = get_extra_account_metas_address(mint_pubkey, program_id);
65    let validate_state_data = fetch_account_data_fn(validate_state_pubkey)
66        .await?
67        .ok_or(ProgramError::InvalidAccountData)?;
68
69    // Check to make sure the provided keys are in the instruction
70    if [
71        source_pubkey,
72        mint_pubkey,
73        destination_pubkey,
74        authority_pubkey,
75    ]
76    .iter()
77    .any(|&key| !instruction.accounts.iter().any(|meta| meta.pubkey == *key))
78    {
79        Err(TransferHookError::IncorrectAccount)?;
80    }
81
82    let mut execute_instruction = execute(
83        program_id,
84        source_pubkey,
85        mint_pubkey,
86        destination_pubkey,
87        authority_pubkey,
88        &validate_state_pubkey,
89        amount,
90    );
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}