additional_accounts_request/
lib.rs

1//! This library provides a way for Solana programs to request additional accounts
2//! for an instruction, according to sRFC 21.
3use anchor_lang::prelude::*;
4use anchor_lang::solana_program::log::sol_log_compute_units;
5use anchor_lang::solana_program::program::MAX_RETURN_DATA;
6use anchor_lang::solana_program::{
7    hash,
8    program::{get_return_data, invoke, invoke_signed, set_return_data},
9};
10
11#[derive(Debug, Copy, Clone, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)]
12#[repr(C)]
13pub struct IAccountMeta {
14    pub pubkey: Pubkey,
15    pub writable: u8,
16}
17
18pub const MAX_ACCOUNTS: usize = 30;
19
20#[zero_copy]
21#[derive(Debug, AnchorDeserialize, AnchorSerialize)]
22pub struct AdditionalAccounts {
23    pub protocol_version: u8,
24    pub has_more: u8,
25    pub _padding_1: [u8; 2],
26    pub num_accounts: u32,
27    pub accounts: [Pubkey; MAX_ACCOUNTS],
28    pub writable_bits: [u8; MAX_ACCOUNTS],
29    pub _padding_2: [u8; 26],
30}
31
32impl Default for AdditionalAccounts {
33    fn default() -> Self {
34        Self {
35            protocol_version: 0,
36            has_more: 0,
37            _padding_1: [0u8; 2],
38            num_accounts: 0u32,
39            accounts: [Pubkey::default(); MAX_ACCOUNTS],
40            writable_bits: [0u8; MAX_ACCOUNTS],
41            _padding_2: [0u8; 26],
42        }
43    }
44}
45
46impl AdditionalAccounts {
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    pub fn has_space_available(&self) -> bool {
52        MAX_ACCOUNTS - self.num_accounts as usize > 0
53    }
54
55    pub fn set_has_more(&mut self, has_more: bool) {
56        self.has_more = match has_more {
57            true => 1,
58            false => 0,
59        };
60    }
61
62    pub fn add_account(&mut self, pubkey: &Pubkey, writable: bool) -> Result<()> {
63        if self.num_accounts >= MAX_ACCOUNTS as u32 {
64            msg!("Cannot write another account");
65            return Err(ProgramError::InvalidInstructionData.into());
66        }
67
68        self.accounts[self.num_accounts as usize] = *pubkey;
69        self.writable_bits[self.num_accounts as usize] = match writable {
70            true => 1,
71            false => 0,
72        };
73        self.num_accounts += 1;
74        Ok(())
75    }
76
77    pub fn iter(&self) -> impl DoubleEndedIterator<Item = (&Pubkey, bool)> {
78        let num_accounts = self.num_accounts as usize;
79        self.accounts[0..num_accounts]
80            .iter()
81            .zip(self.writable_bits[0..num_accounts].iter())
82            .map(|(pubkey, writable)| {
83                (
84                    pubkey,
85                    match writable {
86                        0 => false,
87                        1 => true,
88                        _ => panic!("Invalid writable bit"),
89                    },
90                )
91            })
92    }
93
94    pub fn iter_from(&self, start: usize) -> impl DoubleEndedIterator<Item = (&Pubkey, bool)> {
95        let num_accounts = self.num_accounts as usize;
96        self.accounts[start..num_accounts]
97            .iter()
98            .zip(self.writable_bits[0..num_accounts].iter())
99            .map(|(pubkey, writable)| {
100                (
101                    pubkey,
102                    match writable {
103                        0 => false,
104                        1 => true,
105                        _ => panic!("Invalid writable bit"),
106                    },
107                )
108            })
109    }
110
111    pub fn from_return_data(data: &[u8]) -> Result<&Self> {
112        if data.len() != MAX_RETURN_DATA {
113            msg!("Invalid return data length");
114            return Err(ProgramError::InvalidAccountData.into());
115        }
116        Ok(bytemuck::from_bytes::<AdditionalAccounts>(&data))
117    }
118}
119
120/// Resolves the page of accounts for a particular instruction
121#[inline(never)]
122pub fn resolve_additional_accounts<'info, C1: ToAccountInfos<'info> + ToAccountMetas>(
123    ix_name: String,
124    ctx: &CpiContext<'_, '_, '_, 'info, C1>,
125    args: &[u8],
126    log_info: bool,
127) -> Result<AdditionalAccounts> {
128    call_preflight_interface_function(ix_name.clone(), &ctx, &args)?;
129
130    let program_key = ctx.program.key();
131    let (key, program_data) = get_return_data().unwrap();
132    assert_eq!(key, program_key);
133
134    let program_data = program_data.as_slice();
135    if log_info {
136        msg!("Return data length: {}", program_data.len());
137    }
138
139    // Program return data actually may be unaligned on the stack
140    // so we can't do our normal bytemuck::from_bytes call here
141    let accs: AdditionalAccounts = bytemuck::pod_read_unaligned::<AdditionalAccounts>(program_data);
142    if log_info {
143        msg!(
144            "Accounts has more: {} {}",
145            accs.has_more,
146            accs.accounts.len()
147        );
148    }
149    Ok(accs)
150}
151
152/// Returns the additional accounts needed to execute the instruction
153/// Will only return up to MAX_ACCOUNTS accounts.
154pub fn identify_additional_accounts<'info, C1: ToAccountInfos<'info> + ToAccountMetas>(
155    ix_name: String,
156    ctx: &CpiContext<'_, '_, '_, 'info, C1>,
157    args: &[u8],
158    log_info: bool,
159) -> Result<Vec<AdditionalAccounts>> {
160    if log_info {
161        msg!("Preflight {}", &ix_name);
162    }
163
164    let mut additional_accounts: Vec<AdditionalAccounts> = vec![];
165
166    // This is really meant to page all accounts, page by page
167    // to get all the account metas to send
168    let mut has_more = true;
169    while has_more {
170        let accs = resolve_additional_accounts(ix_name.clone(), ctx, args, log_info)?;
171
172        additional_accounts.push(accs);
173
174        // If we are missing any of the requested accounts, we should exit
175        let mut should_exit = false;
176        accs.iter().rev().for_each(|(acc, _writable)| {
177            let mut found = false;
178            ctx.remaining_accounts.iter().rev().for_each(|account| {
179                if account.key == acc {
180                    found = true;
181                }
182            });
183            if !found {
184                should_exit = true;
185            }
186        });
187        if should_exit {
188            msg!("Missing account(s)");
189            break;
190        }
191
192        has_more = accs.has_more == 1;
193    }
194
195    Ok(additional_accounts)
196}
197
198/// This calls the preflight function on the target program (defined on the ctx)
199pub fn call_preflight_interface_function<'info, T: ToAccountInfos<'info> + ToAccountMetas>(
200    function_name: String,
201    ctx: &CpiContext<'_, '_, '_, 'info, T>,
202    args: &[u8],
203) -> Result<()> {
204    // setup
205    sol_log_compute_units();
206    let mut ix_data: Vec<u8> =
207        hash::hash(format!("global:preflight_{}", &function_name).as_bytes()).to_bytes()[..8]
208            .to_vec();
209
210    ix_data.extend_from_slice(args);
211
212    let mut ix_account_metas = ctx.accounts.to_account_metas(Some(false));
213    ix_account_metas.extend(ctx.remaining_accounts.to_account_metas(None));
214
215    let ix = anchor_lang::solana_program::instruction::Instruction {
216        program_id: ctx.program.key(),
217        accounts: ix_account_metas,
218        data: ix_data,
219    };
220    sol_log_compute_units();
221    msg!("Preflighted...");
222
223    // execute
224    let mut ix_ais = ctx.accounts.to_account_infos();
225    ix_ais.extend(ctx.remaining_accounts.to_account_infos());
226    invoke(&ix, &ix_ais)?;
227    Ok(())
228}
229
230pub fn call_interface_function_raw(
231    program_key: &Pubkey,
232    function_name: String,
233    args: &[u8],
234    metas: Vec<AccountMeta>,
235    accounts: &[AccountInfo],
236    signer_seeds: &[&[&[u8]]],
237    log_info: bool,
238) -> Result<()> {
239    let mut ix_data: Vec<u8> =
240        hash::hash(format!("global:{}", &function_name).as_bytes()).to_bytes()[..8].to_vec();
241    ix_data.extend_from_slice(&args);
242
243    if log_info {
244        msg!("Account Metas creation...");
245        sol_log_compute_units();
246    }
247    if log_info {
248        sol_log_compute_units();
249        msg!("Account Metas created...");
250    }
251
252    let ix = anchor_lang::solana_program::instruction::Instruction {
253        program_id: *program_key,
254        accounts: metas,
255        data: ix_data,
256    };
257
258    // Oddly enough, we only need to specify the account metas
259    // we can just throw the account infos in there and account metas
260    // will specify ordering & filtering
261    if log_info {
262        msg!("Finished creating context...");
263        sol_log_compute_units();
264    }
265
266    invoke_signed(&ix, &accounts, &signer_seeds)?;
267    Ok(())
268}
269
270/// This calls the main function on the target program, and passes along the requested
271/// account_metas from the preflight function
272pub fn call_interface_function<'info, T: ToAccountInfos<'info> + ToAccountMetas>(
273    function_name: String,
274    ctx: CpiContext<'_, '_, '_, 'info, T>,
275    args: &[u8],
276    additional_accounts: &mut dyn Iterator<Item = (&Pubkey, bool)>,
277    log_info: bool,
278) -> Result<()> {
279    if log_info {
280        msg!("Creating interface context...");
281        sol_log_compute_units();
282    }
283    // setup
284    let remaining_accounts = ctx.remaining_accounts.to_vec();
285
286    let mut ix_data: Vec<u8> =
287        hash::hash(format!("global:{}", &function_name).as_bytes()).to_bytes()[..8].to_vec();
288    ix_data.extend_from_slice(&args);
289
290    if log_info {
291        msg!("Account Metas creation...");
292        sol_log_compute_units();
293    }
294    let mut ix_account_metas = ctx.accounts.to_account_metas(None);
295    ix_account_metas.append(
296        additional_accounts
297            .map(|(acc, writable)| {
298                if writable {
299                    AccountMeta::new(*acc, false)
300                } else {
301                    AccountMeta::new_readonly(*acc, false)
302                }
303            })
304            .collect::<Vec<AccountMeta>>()
305            .as_mut(),
306    );
307    if log_info {
308        sol_log_compute_units();
309        msg!("Account Metas created...");
310    }
311
312    let ix = anchor_lang::solana_program::instruction::Instruction {
313        program_id: ctx.program.key(),
314        accounts: ix_account_metas,
315        data: ix_data,
316    };
317
318    let mut ix_ais: Vec<AccountInfo> = ctx.accounts.to_account_infos();
319    if log_info {
320        msg!("IX accounts: {:?}", &ix_ais.len());
321        msg!("Account Info creation...");
322        sol_log_compute_units();
323    }
324    // Oddly enough, we only need to specify the account metas
325    // we can just throw the account infos in there and account metas
326    // will specify ordering & filtering (?)
327    ix_ais.extend_from_slice(&remaining_accounts);
328    if log_info {
329        sol_log_compute_units();
330        msg!("Account Infos created...");
331    }
332
333    if log_info {
334        msg!("IX accounts: {:?}", &ix_ais.len());
335        // ix_ais.iter().into_iter().for_each(|ai| {
336        //     msg!(
337        //         "Account: {:?}, {:?}, {:?}, {:?}",
338        //         ai.key,
339        //         ai.owner,
340        //         ai.is_signer,
341        //         ai.is_writable
342        //     )
343        // });
344        // msg!("Signer seeds: {:?}", &ctx.signer_seeds);
345    }
346
347    if log_info {
348        msg!("Finished creating context...");
349        sol_log_compute_units();
350    }
351
352    // execute
353    invoke_signed(&ix, &ix_ais, &ctx.signer_seeds)?;
354    Ok(())
355}
356
357pub fn get_delimiter(program_id: &Pubkey) -> Pubkey {
358    Pubkey::find_program_address(&["DELIMITER".as_ref()], program_id).0
359}
360
361/// Calls an instruction on a program that complies with the additional accounts interface
362///
363/// Expects ctx.remaining accounts to have all possible accounts in order to resolve
364/// the accounts requested from the preflight function
365#[inline(never)]
366pub fn call<'info, C1: ToAccountInfos<'info> + ToAccountMetas>(
367    ix_name: String,
368    ctx: CpiContext<'_, '_, '_, 'info, C1>,
369    args: Vec<u8>,
370    delimiter: Pubkey,
371    num_accounts_consumed: u8,
372    log_info: bool,
373) -> Result<u8> {
374    // preflight
375    let mut accounts = ctx.accounts.to_account_infos();
376    let mut metas = ctx.accounts.to_account_metas(None);
377
378    if log_info {
379        msg!("Identifying additional accounts...");
380        sol_log_compute_units();
381    }
382    let mut used_accounts = 0;
383    for acc in ctx.remaining_accounts[num_accounts_consumed as usize..].iter() {
384        used_accounts += 1;
385        if *acc.key != delimiter {
386            accounts.push(acc.clone());
387            metas.push(AccountMeta {
388                pubkey: *acc.key,
389                is_signer: acc.is_signer,
390                is_writable: acc.is_writable,
391            });
392        } else {
393            if log_info {
394                msg!("Found delimiter");
395            }
396            break;
397        }
398    }
399
400    // execute
401    if log_info {
402        sol_log_compute_units();
403        msg!("Execute {}", &ix_name);
404    }
405    call_interface_function_raw(
406        ctx.program.key,
407        ix_name.clone(),
408        &args,
409        metas,
410        &accounts,
411        ctx.signer_seeds,
412        log_info,
413    )?;
414    Ok(num_accounts_consumed + used_accounts)
415}
416
417pub fn forward_return_data(expected_program_key: &Pubkey) {
418    let (key, return_data) = get_return_data().unwrap();
419    assert_eq!(key, *expected_program_key);
420    set_return_data(&return_data);
421}
422
423pub trait InterfaceInstruction {
424    fn instruction_name() -> String;
425}