Skip to main content

hopper_runtime/
remaining.rs

1//! Remaining-accounts accessor with strict and passthrough modes.
2//!
3//! The declared context validates exactly `ACCOUNT_COUNT` accounts.
4//! Any accounts beyond that index are "remaining": pool participants,
5//! keeper bot recipients, arbitrary fanout destinations, remainder
6//! destinations for sweeps, and so on. Hopper exposes two ways to
7//! consume them.
8//!
9//! ## Strict mode
10//!
11//! Default. The accessor rejects any remaining account whose address
12//! matches a previously seen account (either declared or already
13//! yielded). Protects against accidental double-spending when a
14//! caller tries to alias one slot into two different roles.
15//!
16//! ```ignore
17//! let rem = ctx.remaining_accounts();
18//! for maybe_acc in rem.iter() {
19//!     let acc = maybe_acc?; // errors on duplicate
20//!     // ...
21//! }
22//! ```
23//!
24//! ## Passthrough mode
25//!
26//! Opt-in. Preserves duplicates verbatim. Use when the caller is
27//! expected to pass the same account in multiple roles (batched CPI
28//! fan-in, for example).
29//!
30//! ```ignore
31//! let rem = ctx.remaining_accounts_passthrough();
32//! ```
33//!
34//! Both modes are O(n) with no heap and no syscalls. Strict mode
35//! keeps a small const-sized seen-address cache sized at 64; past
36//! that, it falls back to a linear scan of the declared slice plus
37//! the yielded-view cursor.
38
39use crate::{
40    account::AccountView,
41    account_wrappers::{Signer, UncheckedAccount},
42    error::ProgramError,
43    foreign::{ExternalAccount, ExternalZeroCopy},
44    ProgramResult,
45};
46
47/// Upper bound on remaining-account iterator length. Matches Quasar's
48/// `MAX_REMAINING_ACCOUNTS` so programs porting from one framework to
49/// the other see the same ceiling. Exceeding this returns an error
50/// rather than risking unbounded stack usage in the seen-address cache.
51pub const MAX_REMAINING_ACCOUNTS: usize = 64;
52
53/// Error surface for the remaining-accounts accessor.
54#[derive(Copy, Clone, Debug, PartialEq, Eq)]
55pub enum RemainingError {
56    /// Two remaining-account slots resolved to the same address, or a
57    /// remaining-account address matched an already-declared account.
58    /// Only strict mode emits this.
59    DuplicateAccount,
60    /// More than [`MAX_REMAINING_ACCOUNTS`] were accessed via the
61    /// iterator.
62    Overflow,
63}
64
65impl From<RemainingError> for ProgramError {
66    fn from(e: RemainingError) -> Self {
67        match e {
68            RemainingError::DuplicateAccount => ProgramError::InvalidAccountData,
69            RemainingError::Overflow => ProgramError::InvalidArgument,
70        }
71    }
72}
73
74/// Duplicate-handling policy for a [`RemainingAccounts`] view.
75#[derive(Copy, Clone, Eq, PartialEq, Debug)]
76pub enum RemainingMode {
77    /// Reject any yielded account whose address matches a declared or
78    /// previously-yielded account. Safe default for pool programs
79    /// and anything that intends every slot to be distinct.
80    Strict,
81    /// Yield every slot as is. Use when the caller is expected to
82    /// pass aliases (batched fan-in, self-transfers, etc.).
83    Passthrough,
84}
85
86/// Zero-allocation remaining-accounts view.
87///
88/// Construct via [`RemainingAccounts::strict`] or
89/// [`RemainingAccounts::passthrough`] from the declared slice and the
90/// full accounts slice. `#[hopper::context]` emits
91/// `ctx.remaining_accounts()` and `ctx.remaining_accounts_passthrough()`
92/// accessors that wire these up for you.
93pub struct RemainingAccounts<'a> {
94    /// Already-validated context accounts, used for dedup in strict mode.
95    declared: &'a [AccountView<'a>],
96    /// Accounts beyond the declared count.
97    remaining: &'a [AccountView<'a>],
98    /// Duplicate-handling policy.
99    mode: RemainingMode,
100}
101
102impl<'a> RemainingAccounts<'a> {
103    /// Build a strict accessor. Iteration rejects duplicates.
104    #[inline(always)]
105    pub fn strict(declared: &'a [AccountView<'a>], remaining: &'a [AccountView<'a>]) -> Self {
106        Self {
107            declared,
108            remaining,
109            mode: RemainingMode::Strict,
110        }
111    }
112
113    /// Build a passthrough accessor. Iteration preserves duplicates.
114    #[inline(always)]
115    pub fn passthrough(declared: &'a [AccountView<'a>], remaining: &'a [AccountView<'a>]) -> Self {
116        Self {
117            declared,
118            remaining,
119            mode: RemainingMode::Passthrough,
120        }
121    }
122
123    /// Length of the remaining slice, irrespective of mode.
124    #[inline(always)]
125    pub fn len(&self) -> usize {
126        self.remaining.len()
127    }
128
129    /// True when there are no remaining accounts.
130    #[inline(always)]
131    pub fn is_empty(&self) -> bool {
132        self.remaining.is_empty()
133    }
134
135    /// The active duplicate-handling policy for this view.
136    #[inline(always)]
137    pub fn mode(&self) -> RemainingMode {
138        self.mode
139    }
140
141    /// The raw remaining-account slice backing this view.
142    #[inline(always)]
143    pub fn as_slice(&self) -> &'a [AccountView<'a>] {
144        self.remaining
145    }
146
147    /// Random access by index. Passthrough returns the slot as is;
148    /// strict returns an error when the resolved slot aliases a
149    /// previously-seen account (declared or yielded before `index`).
150    pub fn get(&self, index: usize) -> Result<Option<&'a AccountView<'a>>, ProgramError> {
151        if index >= self.remaining.len() {
152            return Ok(None);
153        }
154        let candidate = &self.remaining[index];
155        match self.mode {
156            RemainingMode::Passthrough => Ok(Some(candidate)),
157            RemainingMode::Strict => {
158                if index >= MAX_REMAINING_ACCOUNTS {
159                    return Err(RemainingError::Overflow.into());
160                }
161                // Scan declared.
162                for d in self.declared {
163                    if d.address() == candidate.address() {
164                        return Err(RemainingError::DuplicateAccount.into());
165                    }
166                }
167                // Scan remaining[0..index].
168                for r in &self.remaining[..index] {
169                    if r.address() == candidate.address() {
170                        return Err(RemainingError::DuplicateAccount.into());
171                    }
172                }
173                Ok(Some(candidate))
174            }
175        }
176    }
177
178    /// Validate the remaining tail as at most `N` account views.
179    ///
180    /// In strict mode this also rejects aliases to declared accounts
181    /// and duplicate remaining slots before returning the typed set.
182    pub fn account_views<const N: usize>(
183        &self,
184    ) -> Result<RemainingAccountViews<'a, N>, ProgramError> {
185        if self.remaining.len() > N {
186            return Err(RemainingError::Overflow.into());
187        }
188        let mut items: [Option<&'a AccountView<'a>>; N] = [None; N];
189        let mut index = 0;
190        while index < self.remaining.len() {
191            let account = self.get(index)?.ok_or(ProgramError::NotEnoughAccountKeys)?;
192            items[index] = Some(account);
193            index += 1;
194        }
195        Ok(RemainingAccountViews { items, len: index })
196    }
197
198    /// Validate the remaining tail as at most `N` signer accounts.
199    ///
200    /// This is the common multisig case: the handler gets a bounded,
201    /// duplicate-safe signer set instead of raw account iteration.
202    pub fn signers<const N: usize>(&self) -> Result<RemainingSigners<'a, N>, ProgramError> {
203        if self.remaining.len() > N {
204            return Err(RemainingError::Overflow.into());
205        }
206        let mut items: [Option<Signer<'a>>; N] = [None; N];
207        let mut index = 0;
208        while index < self.remaining.len() {
209            let account = self.get(index)?.ok_or(ProgramError::NotEnoughAccountKeys)?;
210            items[index] = Some(Signer::try_new(account)?);
211            index += 1;
212        }
213        Ok(RemainingSigners { items, len: index })
214    }
215
216    /// Sequential iterator. Yields each account in declaration order,
217    /// errors on duplicates in strict mode, preserves them in
218    /// passthrough mode.
219    #[inline(always)]
220    pub fn iter(&self) -> RemainingIter<'a> {
221        RemainingIter {
222            declared: self.declared,
223            remaining: self.remaining,
224            mode: self.mode,
225            index: 0,
226        }
227    }
228
229    /// Sequential typed parser over the remaining-account tail.
230    ///
231    /// This preserves the current duplicate policy while letting handlers bind
232    /// each slot as a signer, raw account, or known external account in the
233    /// order protocol instructions naturally expect.
234    #[inline(always)]
235    pub fn typed(&self) -> RemainingTyped<'a> {
236        RemainingTyped {
237            declared: self.declared,
238            remaining: self.remaining,
239            mode: self.mode,
240            index: 0,
241        }
242    }
243
244    /// Random-access lazy parser over the remaining-account tail.
245    ///
246    /// This is useful when an instruction receives many optional accounts but
247    /// validates only the branch it actually touches. The selected slot still
248    /// inherits this view's duplicate policy.
249    #[inline(always)]
250    pub fn lazy(&self) -> RemainingLazy<'a> {
251        RemainingLazy {
252            declared: self.declared,
253            remaining: self.remaining,
254            mode: self.mode,
255        }
256    }
257}
258
259/// Iterator yielded by [`RemainingAccounts::iter`].
260pub struct RemainingIter<'a> {
261    declared: &'a [AccountView<'a>],
262    remaining: &'a [AccountView<'a>],
263    mode: RemainingMode,
264    index: usize,
265}
266
267impl<'a> Iterator for RemainingIter<'a> {
268    type Item = Result<&'a AccountView<'a>, ProgramError>;
269
270    fn next(&mut self) -> Option<Self::Item> {
271        if self.index >= self.remaining.len() {
272            return None;
273        }
274        if self.index >= MAX_REMAINING_ACCOUNTS {
275            // Pin the cursor so repeated calls after overflow stay
276            // cheap and deterministic.
277            self.index = self.remaining.len();
278            return Some(Err(RemainingError::Overflow.into()));
279        }
280        let candidate = &self.remaining[self.index];
281        let i = self.index;
282        self.index = self.index.wrapping_add(1);
283
284        if matches!(self.mode, RemainingMode::Strict) {
285            for d in self.declared {
286                if d.address() == candidate.address() {
287                    return Some(Err(RemainingError::DuplicateAccount.into()));
288                }
289            }
290            for r in &self.remaining[..i] {
291                if r.address() == candidate.address() {
292                    return Some(Err(RemainingError::DuplicateAccount.into()));
293                }
294            }
295        }
296        Some(Ok(candidate))
297    }
298}
299
300/// Bounded, validated remaining account-view set.
301pub struct RemainingAccountViews<'a, const N: usize> {
302    items: [Option<&'a AccountView<'a>>; N],
303    len: usize,
304}
305
306impl<'a, const N: usize> RemainingAccountViews<'a, N> {
307    /// Number of parsed account views.
308    #[inline(always)]
309    pub const fn len(&self) -> usize {
310        self.len
311    }
312
313    /// True when the parsed set is empty.
314    #[inline(always)]
315    pub const fn is_empty(&self) -> bool {
316        self.len == 0
317    }
318
319    /// Return account `index` if it exists.
320    #[inline(always)]
321    pub fn get(&self, index: usize) -> Option<&'a AccountView<'a>> {
322        if index >= self.len {
323            None
324        } else {
325            self.items[index]
326        }
327    }
328
329    /// Iterate over the parsed account views.
330    #[inline(always)]
331    pub fn iter(&self) -> RemainingAccountViewIter<'_, 'a, N> {
332        RemainingAccountViewIter {
333            set: self,
334            index: 0,
335        }
336    }
337}
338
339/// Iterator over a bounded account-view set.
340pub struct RemainingAccountViewIter<'set, 'a, const N: usize> {
341    set: &'set RemainingAccountViews<'a, N>,
342    index: usize,
343}
344
345impl<'a, const N: usize> Iterator for RemainingAccountViewIter<'_, 'a, N> {
346    type Item = &'a AccountView<'a>;
347
348    fn next(&mut self) -> Option<Self::Item> {
349        if self.index >= self.set.len {
350            return None;
351        }
352        let item = self.set.items[self.index];
353        self.index += 1;
354        item
355    }
356}
357
358/// Bounded, validated remaining signer set.
359pub struct RemainingSigners<'a, const N: usize> {
360    items: [Option<Signer<'a>>; N],
361    len: usize,
362}
363
364impl<'a, const N: usize> RemainingSigners<'a, N> {
365    /// Number of parsed signers.
366    #[inline(always)]
367    pub const fn len(&self) -> usize {
368        self.len
369    }
370
371    /// True when the parsed set is empty.
372    #[inline(always)]
373    pub const fn is_empty(&self) -> bool {
374        self.len == 0
375    }
376
377    /// Return signer `index` if it exists.
378    #[inline(always)]
379    pub fn get(&self, index: usize) -> Option<Signer<'a>> {
380        if index >= self.len {
381            None
382        } else {
383            self.items[index]
384        }
385    }
386
387    /// Iterate over the parsed signers.
388    #[inline(always)]
389    pub fn iter(&self) -> RemainingSignerIter<'_, 'a, N> {
390        RemainingSignerIter {
391            set: self,
392            index: 0,
393        }
394    }
395}
396
397/// Iterator over a bounded signer set.
398pub struct RemainingSignerIter<'set, 'a, const N: usize> {
399    set: &'set RemainingSigners<'a, N>,
400    index: usize,
401}
402
403impl<'a, const N: usize> Iterator for RemainingSignerIter<'_, 'a, N> {
404    type Item = Signer<'a>;
405
406    fn next(&mut self) -> Option<Self::Item> {
407        if self.index >= self.set.len {
408            return None;
409        }
410        let item = self.set.items[self.index];
411        self.index += 1;
412        item
413    }
414}
415
416/// Sequential typed parser for remaining accounts.
417pub struct RemainingTyped<'a> {
418    declared: &'a [AccountView<'a>],
419    remaining: &'a [AccountView<'a>],
420    mode: RemainingMode,
421    index: usize,
422}
423
424impl<'a> RemainingTyped<'a> {
425    #[inline(always)]
426    fn view(&self) -> RemainingAccounts<'a> {
427        RemainingAccounts {
428            declared: self.declared,
429            remaining: self.remaining,
430            mode: self.mode,
431        }
432    }
433
434    /// Number of slots already consumed by typed parsing.
435    #[inline(always)]
436    pub const fn consumed(&self) -> usize {
437        self.index
438    }
439
440    /// Number of unconsumed remaining slots.
441    #[inline(always)]
442    pub fn remaining_len(&self) -> usize {
443        self.remaining.len().saturating_sub(self.index)
444    }
445
446    /// True when all remaining slots have been consumed.
447    #[inline(always)]
448    pub fn is_empty(&self) -> bool {
449        self.index >= self.remaining.len()
450    }
451
452    /// Consume and return the next raw account view.
453    pub fn next_account(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
454        let account = self
455            .view()
456            .get(self.index)?
457            .ok_or(ProgramError::NotEnoughAccountKeys)?;
458        self.index += 1;
459        Ok(account)
460    }
461
462    /// Consume and return the next raw account view as an explicit unchecked role.
463    #[inline]
464    pub fn next_unchecked(&mut self) -> Result<UncheckedAccount<'a>, ProgramError> {
465        Ok(UncheckedAccount::new(self.next_account()?))
466    }
467
468    /// Consume and validate the next account as a signer.
469    #[inline]
470    pub fn next_signer(&mut self) -> Result<Signer<'a>, ProgramError> {
471        Signer::try_new(self.next_account()?)
472    }
473
474    /// Consume and validate the next account as a known external layout.
475    #[inline]
476    pub fn next_external<T: ExternalZeroCopy>(
477        &mut self,
478    ) -> Result<ExternalAccount<'a, T>, ProgramError> {
479        ExternalAccount::try_new(self.next_account()?)
480    }
481
482    /// Fluent duplicate-policy gate for typed remaining parsing.
483    #[inline]
484    pub fn no_duplicates(self) -> Result<Self, ProgramError> {
485        self.assert_no_duplicates()?;
486        Ok(self)
487    }
488
489    /// Consume a fixed-size sequential group from the remaining tail.
490    ///
491    /// Call [`Self::no_duplicates`] first when the whole tail must be globally
492    /// alias-free across several groups.
493    pub fn take_group(&mut self, len: usize) -> Result<RemainingGroup<'a>, ProgramError> {
494        let end = self
495            .index
496            .checked_add(len)
497            .ok_or(ProgramError::ArithmeticOverflow)?;
498        if end > self.remaining.len() {
499            return Err(ProgramError::NotEnoughAccountKeys);
500        }
501        let group = RemainingGroup {
502            parser: RemainingTyped {
503                declared: self.declared,
504                remaining: &self.remaining[self.index..end],
505                mode: self.mode,
506                index: 0,
507            },
508        };
509        self.index = end;
510        Ok(group)
511    }
512
513    /// Verify every remaining slot is distinct from declared and sibling slots.
514    pub fn assert_no_duplicates(&self) -> ProgramResult {
515        let strict = RemainingAccounts {
516            declared: self.declared,
517            remaining: self.remaining,
518            mode: RemainingMode::Strict,
519        };
520        let mut index = 0;
521        while index < self.remaining.len() {
522            strict
523                .get(index)?
524                .ok_or(ProgramError::NotEnoughAccountKeys)?;
525            index += 1;
526        }
527        Ok(())
528    }
529
530    /// Verify all remaining accounts are sorted by a caller-supplied key.
531    pub fn assert_sorted_by<K, F>(&self, mut key: F) -> ProgramResult
532    where
533        K: Ord,
534        F: FnMut(&'a AccountView<'a>) -> Result<K, ProgramError>,
535    {
536        let view = self.view();
537        let mut previous: Option<K> = None;
538        let mut index = 0;
539        while index < self.remaining.len() {
540            let account = view.get(index)?.ok_or(ProgramError::NotEnoughAccountKeys)?;
541            let current = key(account)?;
542            if let Some(ref last) = previous {
543                if current < *last {
544                    return Err(ProgramError::InvalidAccountData);
545                }
546            }
547            previous = Some(current);
548            index += 1;
549        }
550        Ok(())
551    }
552
553    /// Require the typed parser to have consumed the full tail.
554    #[inline]
555    pub fn assert_empty(&self) -> ProgramResult {
556        if self.is_empty() {
557            Ok(())
558        } else {
559            Err(ProgramError::InvalidArgument)
560        }
561    }
562}
563
564/// Sequential typed parser for one logical remaining-account group.
565pub struct RemainingGroup<'a> {
566    parser: RemainingTyped<'a>,
567}
568
569impl<'a> RemainingGroup<'a> {
570    /// Number of unconsumed group slots.
571    #[inline(always)]
572    pub fn remaining_len(&self) -> usize {
573        self.parser.remaining_len()
574    }
575
576    /// Consume and return the next account in this group.
577    #[inline]
578    pub fn next_account(&mut self) -> Result<&'a AccountView<'a>, ProgramError> {
579        self.parser.next_account()
580    }
581
582    /// Consume and validate the next group account as a signer.
583    #[inline]
584    pub fn next_signer(&mut self) -> Result<Signer<'a>, ProgramError> {
585        self.parser.next_signer()
586    }
587
588    /// Consume and validate the next group account as a known external layout.
589    #[inline]
590    pub fn next_external<T: ExternalZeroCopy>(
591        &mut self,
592    ) -> Result<ExternalAccount<'a, T>, ProgramError> {
593        self.parser.next_external::<T>()
594    }
595
596    /// Parse the entire group as at most `N` known external accounts.
597    pub fn parse_external<T: ExternalZeroCopy, const N: usize>(
598        &mut self,
599    ) -> Result<RemainingExternalAccounts<'a, T, N>, ProgramError> {
600        if self.parser.remaining_len() > N {
601            return Err(RemainingError::Overflow.into());
602        }
603        let mut items: [Option<ExternalAccount<'a, T>>; N] = [None; N];
604        let mut len = 0;
605        while !self.parser.is_empty() {
606            items[len] = Some(self.parser.next_external::<T>()?);
607            len += 1;
608        }
609        Ok(RemainingExternalAccounts { items, len })
610    }
611
612    /// Require the group parser to have consumed every slot.
613    #[inline]
614    pub fn assert_empty(&self) -> ProgramResult {
615        self.parser.assert_empty()
616    }
617}
618
619/// Bounded parsed external-account group.
620pub struct RemainingExternalAccounts<'a, T: ExternalZeroCopy, const N: usize> {
621    items: [Option<ExternalAccount<'a, T>>; N],
622    len: usize,
623}
624
625impl<'a, T: ExternalZeroCopy, const N: usize> RemainingExternalAccounts<'a, T, N> {
626    /// Number of parsed external accounts.
627    #[inline(always)]
628    pub const fn len(&self) -> usize {
629        self.len
630    }
631
632    /// True when the parsed group is empty.
633    #[inline(always)]
634    pub const fn is_empty(&self) -> bool {
635        self.len == 0
636    }
637
638    /// Return parsed external account `index` if it exists.
639    #[inline(always)]
640    pub fn get(&self, index: usize) -> Option<ExternalAccount<'a, T>> {
641        if index >= self.len {
642            None
643        } else {
644            self.items[index]
645        }
646    }
647}
648
649/// Random-access lazy parser for remaining accounts.
650pub struct RemainingLazy<'a> {
651    declared: &'a [AccountView<'a>],
652    remaining: &'a [AccountView<'a>],
653    mode: RemainingMode,
654}
655
656impl<'a> RemainingLazy<'a> {
657    #[inline(always)]
658    fn view(&self) -> RemainingAccounts<'a> {
659        RemainingAccounts {
660            declared: self.declared,
661            remaining: self.remaining,
662            mode: self.mode,
663        }
664    }
665
666    /// Number of remaining slots available for lazy access.
667    #[inline(always)]
668    pub fn len(&self) -> usize {
669        self.remaining.len()
670    }
671
672    /// True when there are no remaining slots.
673    #[inline(always)]
674    pub fn is_empty(&self) -> bool {
675        self.remaining.is_empty()
676    }
677
678    /// Select a remaining-account slot by index without validating any other
679    /// external accounts in the tail.
680    pub fn at(&self, index: usize) -> Result<RemainingLazySlot<'a>, ProgramError> {
681        let account = self
682            .view()
683            .get(index)?
684            .ok_or(ProgramError::NotEnoughAccountKeys)?;
685        Ok(RemainingLazySlot { account })
686    }
687}
688
689/// One lazily-selected remaining-account slot.
690pub struct RemainingLazySlot<'a> {
691    account: &'a AccountView<'a>,
692}
693
694impl<'a> RemainingLazySlot<'a> {
695    /// Return the raw account view.
696    #[inline(always)]
697    pub const fn account(&self) -> &'a AccountView<'a> {
698        self.account
699    }
700
701    /// Bind this slot as an unchecked account role.
702    #[inline(always)]
703    pub fn unchecked(&self) -> UncheckedAccount<'a> {
704        UncheckedAccount::new(self.account)
705    }
706
707    /// Validate this slot as a signer.
708    #[inline]
709    pub fn signer(&self) -> Result<Signer<'a>, ProgramError> {
710        Signer::try_new(self.account)
711    }
712
713    /// Validate this slot as a known external layout.
714    #[inline]
715    pub fn external<T: ExternalZeroCopy>(&self) -> Result<ExternalAccount<'a, T>, ProgramError> {
716        ExternalAccount::try_new(self.account)
717    }
718}
719
720/// Ergonomic fall-through used by the proc-macro codegen when the user
721/// wants to just burn through remaining accounts without a mode.
722#[inline(always)]
723pub fn strict<'a>(
724    declared: &'a [AccountView<'a>],
725    remaining: &'a [AccountView<'a>],
726) -> RemainingAccounts<'a> {
727    RemainingAccounts::strict(declared, remaining)
728}
729
730#[cfg(test)]
731mod tests {
732    use super::*;
733    use crate::Address;
734    use hopper_native::{
735        AccountView as NativeAccountView, Address as NativeAddress, RuntimeAccount, NOT_BORROWED,
736    };
737    const EXTERNAL_OWNER: Address = Address::new_from_array([5; 32]);
738    struct SampleExternal;
739    impl ExternalZeroCopy for SampleExternal {
740        type View<'a> = crate::foreign::ExternalBytes<'a>;
741
742        const OWNER: Option<Address> = Some(EXTERNAL_OWNER);
743        const DISCRIMINATOR: Option<&'static [u8]> = Some(b"EX");
744        const MIN_LEN: usize = 4;
745
746        fn view<'a>(data: crate::Ref<'a, [u8]>) -> Result<Self::View<'a>, ProgramError> {
747            Ok(crate::foreign::ExternalBytes::new(data))
748        }
749    }
750    fn make_account(
751        address: [u8; 32],
752        owner: Address,
753        signer: bool,
754        data: &[u8],
755    ) -> (std::vec::Vec<u64>, AccountView<'static>) {
756        let mut backing = std::vec![0u64; (RuntimeAccount::SIZE + data.len()).div_ceil(8)];
757        let raw = backing.as_mut_ptr() as *mut RuntimeAccount;
758        // SAFETY: Test helper writes a valid RuntimeAccount header and payload
759        // into owned backing memory that outlives the returned AccountView.
760        unsafe {
761            raw.write(RuntimeAccount {
762                borrow_state: NOT_BORROWED,
763                is_signer: signer as u8,
764                is_writable: 0,
765                executable: 0,
766                resize_delta: 0,
767                address: NativeAddress::new_from_array(address),
768                owner: NativeAddress::new_from_array(owner.to_bytes()),
769                lamports: 1,
770                data_len: data.len() as u64,
771            });
772            let data_ptr = (backing.as_mut_ptr() as *mut u8).add(RuntimeAccount::SIZE);
773            core::ptr::copy_nonoverlapping(data.as_ptr(), data_ptr, data.len());
774        }
775        // SAFETY: `raw` points to the initialized RuntimeAccount header above.
776        let backend = unsafe { NativeAccountView::new_unchecked(raw) };
777        (backing, AccountView::from_backend(backend))
778    }
779
780    // `AccountView` is backend-specific; we cannot construct one under
781    // a non-Solana `cfg`. These tests exist to keep the module
782    // exercised at compile time even when the construction helpers
783    // live behind `target_os = "solana"`.
784
785    #[test]
786    fn error_variants_surface_as_program_error() {
787        let dup: ProgramError = RemainingError::DuplicateAccount.into();
788        assert_eq!(dup, ProgramError::InvalidAccountData);
789        let ovf: ProgramError = RemainingError::Overflow.into();
790        assert_eq!(ovf, ProgramError::InvalidArgument);
791    }
792
793    #[test]
794    fn max_remaining_matches_quasar() {
795        // If we ever change this, also update the remaining-account documentation.
796        assert_eq!(MAX_REMAINING_ACCOUNTS, 64);
797    }
798    #[test]
799    fn typed_remaining_parses_external_signer_and_raw_slots() {
800        let (_declared_backing, declared) =
801            make_account([1; 32], Address::new_from_array([9; 32]), false, b"");
802        let (_external_backing, external) = make_account([2; 32], EXTERNAL_OWNER, false, b"EX12");
803        let (_signer_backing, signer) =
804            make_account([3; 32], Address::new_from_array([9; 32]), true, b"");
805        let (_raw_backing, raw) =
806            make_account([4; 32], Address::new_from_array([9; 32]), false, b"");
807
808        let declared_accounts = [declared];
809        let remaining_accounts = [external, signer, raw];
810        let mut typed = RemainingAccounts::strict(&declared_accounts, &remaining_accounts).typed();
811
812        let external = typed.next_external::<SampleExternal>().unwrap();
813        assert_eq!(external.key(), remaining_accounts[0].address());
814        let signer = typed.next_signer().unwrap();
815        assert_eq!(signer.key(), remaining_accounts[1].address());
816        let raw = typed.next_unchecked().unwrap();
817        assert_eq!(raw.key(), remaining_accounts[2].address());
818        assert!(typed.assert_empty().is_ok());
819    }
820    #[test]
821    fn typed_remaining_supports_groups_and_lazy_external_access() {
822        let (_declared_backing, declared) =
823            make_account([1; 32], Address::new_from_array([9; 32]), false, b"");
824        let (_external_a_backing, external_a) =
825            make_account([2; 32], EXTERNAL_OWNER, false, b"EX12");
826        let (_external_b_backing, external_b) =
827            make_account([3; 32], EXTERNAL_OWNER, false, b"EX34");
828        let (_signer_backing, signer) =
829            make_account([4; 32], Address::new_from_array([9; 32]), true, b"");
830
831        let declared_accounts = [declared];
832        let remaining_accounts = [external_a, external_b, signer];
833        let accounts = RemainingAccounts::strict(&declared_accounts, &remaining_accounts);
834
835        let lazy_external = accounts
836            .lazy()
837            .at(1)
838            .unwrap()
839            .external::<SampleExternal>()
840            .unwrap();
841        assert_eq!(lazy_external.key(), remaining_accounts[1].address());
842
843        let mut typed = accounts.typed().no_duplicates().unwrap();
844        let mut oracle_group = typed.take_group(2).unwrap();
845        let parsed = oracle_group.parse_external::<SampleExternal, 4>().unwrap();
846        assert_eq!(parsed.len(), 2);
847        assert_eq!(
848            parsed.get(0).unwrap().key(),
849            remaining_accounts[0].address()
850        );
851        assert_eq!(
852            parsed.get(1).unwrap().key(),
853            remaining_accounts[1].address()
854        );
855        assert!(oracle_group.assert_empty().is_ok());
856
857        let signer = typed.next_signer().unwrap();
858        assert_eq!(signer.key(), remaining_accounts[2].address());
859        assert!(typed.assert_empty().is_ok());
860    }
861    #[test]
862    fn typed_remaining_duplicate_and_sort_assertions_are_explicit() {
863        let (_declared_backing, declared) =
864            make_account([1; 32], Address::new_from_array([9; 32]), false, b"");
865        let (_duplicate_backing, duplicate) =
866            make_account([1; 32], Address::new_from_array([9; 32]), false, b"");
867        let declared_accounts = [declared];
868        let remaining_accounts = [duplicate];
869        let typed = RemainingAccounts::passthrough(&declared_accounts, &remaining_accounts).typed();
870        assert_eq!(
871            typed.assert_no_duplicates().unwrap_err(),
872            ProgramError::InvalidAccountData
873        );
874
875        let (_a_backing, a) = make_account([3; 32], Address::new_from_array([9; 32]), false, b"");
876        let (_b_backing, b) = make_account([2; 32], Address::new_from_array([9; 32]), false, b"");
877        let remaining_accounts = [a, b];
878        let typed = RemainingAccounts::passthrough(&[], &remaining_accounts).typed();
879        assert_eq!(
880            typed
881                .assert_sorted_by(|account| Ok(account.address().as_bytes()[0]))
882                .unwrap_err(),
883            ProgramError::InvalidAccountData
884        );
885    }
886}