Add account groups and typed remaining accounts#218
Merged
Conversation
Share typed seed parsing across account and standalone seed specs, including bounded fixed byte arrays for PDA seed inputs.
Move PDA signer construction onto the account group that owns the PDA, while keeping contexts as validated accounts plus bumps.
Introduce AccountsArray<T, N> for bounded repeated account groups that preserve typed parsing, bumps, epilogues, and nested account behavior.
Let handlers parse remaining-account tails into capped typed slices with duplicate and declared-account rejection before use.
Defer rent sysvar fetching until init or realloc actually needs the value, while keeping direct borrowed-rent codegen for explicit rent accounts.
Make account-group composition explicit and let typed remaining tails parse account wrappers or derived account groups through the same internal contract.
Parse bounded signer tails at the instruction boundary so the example exercises the new Remaining API instead of raw remaining-account iteration.
BenchmarkVault
Escrow
Multisig
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR tightens Quasar's account composition model and adds bounded typed remaining-account parsing. The goal is to make repeated or trailing account sets ergonomic without moving validation logic into handlers or forcing users back to raw
RemainingAccountsiteration.At a high level this adds:
#[derive(Seeds)]AccountsArray<T, N>for fixed-size repeated account groupsRemaining<T, N>for bounded typed remaining-account tails#[account(custom)]Remaining<Signer, 10>The branch intentionally does not preserve backwards compatibility for superseded escape hatches. The remaining escape hatches are explicit: raw remaining accounts still exist for unbounded/forwarded tails, and fully custom account wrappers can implement the relevant traits directly.
Account-Owned PDA Signers
PDA signer construction now lives with the account group that owns the PDA instead of being materialized eagerly on the context.
Generated account structs with PDA-addressed fields expose signer helpers like:
This keeps the runtime shape lean:
The CPI API now accepts a
CpiSignerSeedssource for single PDA signers. Existing direct multi-signer calls remain available throughinvoke_with_signers.quasar-metadatawas updated for this API boundary: signed metadata helper paths now build explicitSigner::from(seeds)values and callinvoke_with_signers, instead of relying onSeedbeing re-exported from prelude.Typed PDA Seed Parameters
Seed parameter parsing is shared between account-level
#[seeds(...)]and standalone#[derive(Seeds)].Supported typed seed params now include:
Addressu8u16u32u64[u8; N]whereN <= 32The byte-array support is useful for fixed identifiers that should not be forced through scalar encoding or ad hoc handler logic. Prefixes and byte-array params are bounded to Solana's 32-byte seed limit at macro time.
Fixed Account Arrays
AccountsArray<T, N>adds a typed repeated-account group:It consumes exactly
N * T::COUNTaccounts and preserves the normal account-group behavior:ParseAccountsRawThis replaces handler-side loops for fixed repeated groups with a declarative account type. The derive was updated so
AccountsArrayis treated as a composite account group and so nested group parsing writes into the declared account-view buffer without extra intermediate copying.Typed Remaining Accounts
Remaining<T, N>parses a remaining-account tail into a bounded typed slice:It supports two cases:
T::COUNTraw remaining accountsThe parser enforces capacity and chunk completeness. For account wrappers that can expose account data or unchecked views, typed remaining parsing rejects aliases to declared accounts and duplicate remaining aliases. That keeps typed tails from accidentally introducing aliasing hazards.
Signeris intentionally different:Remaining<Signer, N>does not pay duplicate uniqueness scans because signer wrappers do not expose mutable account data. This is account-type-owned behavior, not a handler special case. In the multisig example, duplicate signer metas still do not create extra approvals because approvals are counted against the stored signer set by key.The typed remaining path has a direct single-account fast path. It avoids routing through the public remaining iterator and avoids the iterator's 64-slot duplicate-resolution cache when the item type does not need that machinery.
Multisig Example
The multisig example now exercises the typed API directly:
The instruction handlers no longer manually check signer flags or enforce max count. That work belongs to
Remaining<Signer, 10>and theSigneraccount wrapper.This makes the example a better demonstration of the intended API:
Lazy Rent Resolution
Lifecycle operations now receive an
OpCtx<R>whereR: RentAccessinstead of always receiving a pre-fetched&Rent.This allows three rent sources:
Rent&RentSysvar<Rent>accountRentResolver::fetch_once()when no rent account existsThe derive emits a lazy resolver only when it has to fetch rent itself. If an instruction provides a rent sysvar account, the generated code borrows that account instead. Idempotent/no-op lifecycle paths no longer pay a rent syscall before they know one is needed.
AccountInitnow acceptsInitCtx<'a, R>whereR: RentAccess, and SPL plus metadata init implementations were updated to that contract.Dynamic Account Writes
Dynamic account write paths now accept rent sources through the same
RentAccessabstraction. This keeps realloc/write behavior aligned with init behavior and avoids a separate pre-fetched-rent path just for dynamic account updates.The branch adds compile coverage for dynamic
set_innerwith rent access so this remains wired through the derive surface.Removed
#[account(custom)]#[account(custom)]is removed.The old mode was an early escape hatch that bypassed normal generated validation and lifecycle behavior. With the newer trait boundaries, the cleaner replacement is explicit: implement
AsAccountView,AccountLoad, and any lifecycle traits directly for fully custom wrappers.The macro now emits a compile-time error for
#[account(custom)], and the old compile-pass custom-account test was removed.Internal Traits
This adds a small set of doc-hidden internal traits to support composition without making the public API noisy:
AccountBumps: associates generated account groups with their bump bundleAccountGroup: marker for fixed account groups that can be composed or parsed as typed remaining chunksParseAccountsRaw: raw SVM-buffer parse path used by nested account groups andAccountsArrayRemainingItem: typed remaining-account item contractThese are internal plumbing for the macro and account containers. The public surface remains centered around account types,
AccountsArray<T, N>, andRemaining<T, N>.Performance
Tracked benchmark deltas against
origin/master:The typed multisig path initially cost substantially more. The final version cuts that down by making typed remaining parsing direct, account-type-owned, and inline-friendly. The remaining multisig delta is the cost of the typed bounded API and additional generated account-group machinery.
Commit Structure
The branch is organized as feature commits:
Validation
Local validation run on the final branch:
The pre-push hook also passed its full fmt and Clippy gate before the branch was pushed.