diff --git a/program-libs/compressed-account/src/instruction_data/traits.rs b/program-libs/compressed-account/src/instruction_data/traits.rs index 4bb33c2ae3..ab6e718554 100644 --- a/program-libs/compressed-account/src/instruction_data/traits.rs +++ b/program-libs/compressed-account/src/instruction_data/traits.rs @@ -23,6 +23,7 @@ pub trait InstructionDiscriminator { pub trait LightInstructionData: InstructionDiscriminator + AnchorSerialize { #[cfg(feature = "alloc")] #[profile] + #[inline(never)] fn data(&self) -> Result, CompressedAccountError> { let inputs = AnchorSerialize::try_to_vec(self) .map_err(|_| CompressedAccountError::InvalidArgument)?; diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs index 4828a9ee8a..77c18ff0a3 100644 --- a/sdk-libs/compressible-client/src/lib.rs +++ b/sdk-libs/compressible-client/src/lib.rs @@ -480,7 +480,7 @@ pub mod compressible_instruction { "Tree info index out of bounds - compressed_accounts length must match validity proof accounts length", )?; - let packed_data = data.pack(&mut remaining_accounts); + let packed_data = data.pack(&mut remaining_accounts)?; typed_compressed_accounts.push(CompressedAccountData { meta: CompressedAccountMetaNoLamportsNoAddress { tree_info, diff --git a/sdk-libs/macros/docs/accounts/light_mint.md b/sdk-libs/macros/docs/accounts/light_mint.md index ef40bffab0..29923349cb 100644 --- a/sdk-libs/macros/docs/accounts/light_mint.md +++ b/sdk-libs/macros/docs/accounts/light_mint.md @@ -60,6 +60,57 @@ pub struct CreateMint<'info> { | `rent_payment` | Expression | `2u8` | Rent payment epochs for decompression. | | `write_top_up` | Expression | `0u32` | Write top-up lamports for decompression. | +## TokenMetadata Fields + +Optional fields for creating a mint with the TokenMetadata extension: + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `name` | Expression | - | Token name (expression yielding `Vec`). | +| `symbol` | Expression | - | Token symbol (expression yielding `Vec`). | +| `uri` | Expression | - | Token URI (expression yielding `Vec`). | +| `update_authority` | Field reference | None | Optional update authority for metadata. | +| `additional_metadata` | Expression | None | Additional key-value metadata (expression yielding `Option>`). | + +### Validation Rules + +1. **Core fields are all-or-nothing**: `name`, `symbol`, and `uri` must ALL be specified together, or none at all. +2. **Optional fields require core fields**: `update_authority` and `additional_metadata` require `name`, `symbol`, and `uri` to also be specified. + +### Metadata Example + +```rust +#[light_mint( + mint_signer = mint_signer, + authority = fee_payer, + decimals = 9, + mint_seeds = &[SEED, self.authority.key().as_ref(), &[params.bump]], + // TokenMetadata fields + name = params.name.clone(), + symbol = params.symbol.clone(), + uri = params.uri.clone(), + update_authority = authority, + additional_metadata = params.additional_metadata.clone() +)] +pub cmint: UncheckedAccount<'info>, +``` + +**Invalid configurations (compile-time errors):** + +```rust +// ERROR: name without symbol and uri +#[light_mint( + ..., + name = params.name.clone() +)] + +// ERROR: additional_metadata without name, symbol, uri +#[light_mint( + ..., + additional_metadata = params.additional_metadata.clone() +)] +``` + ## How It Works ### Mint PDA Derivation diff --git a/sdk-libs/macros/docs/features/anchor-account-features.md b/sdk-libs/macros/docs/features/anchor-account-features.md new file mode 100644 index 0000000000..7a5b465257 --- /dev/null +++ b/sdk-libs/macros/docs/features/anchor-account-features.md @@ -0,0 +1,376 @@ +# Anchor Account Macro Features + +This document covers the 14 account constraint features available in the Anchor `#[account]` macro attribute. + +## Overview + +Anchor accounts are defined within a struct marked with `#[derive(Accounts)]`. Each field can have constraints applied via the `#[account(...)]` attribute. + +```rust +#[derive(Accounts)] +pub struct MyInstruction<'info> { + #[account(init, payer = user, space = 8 + 32)] + pub data_account: Account<'info, DataAccount>, + #[account(mut)] + pub user: Signer<'info>, + pub system_program: Program<'info, System>, +} +``` + +--- + +## 1. `init` + +**Purpose**: Creates a new account via CPI to the System Program. + +**Behavior**: +- Allocates space on-chain +- Assigns the account to the program +- Calls `System::create_account` CPI + +**Required companions**: `payer`, `space` + +**Example**: +```rust +#[account(init, payer = user, space = 8 + 64)] +pub my_account: Account<'info, MyData>, +``` + +**Generated code** (simplified): +```rust +let cpi_accounts = system_program::CreateAccount { + from: user.to_account_info(), + to: my_account.to_account_info(), +}; +system_program::create_account(cpi_ctx, lamports, space, program_id)?; +``` + +--- + +## 2. `init_if_needed` + +**Purpose**: Creates account only if it doesn't already exist. + +**Behavior**: +- Checks if account data length is 0 +- If zero, performs `init` +- If non-zero, deserializes existing data + +**Feature flag required**: `init-if-needed` + +**Example**: +```rust +#[account(init_if_needed, payer = user, space = 8 + 64)] +pub my_account: Account<'info, MyData>, +``` + +**Security note**: Can be dangerous with reinitialization attacks if discriminator isn't checked properly. + +--- + +## 3. `zero` + +**Purpose**: Deserializes an account that was pre-allocated (e.g., by an external process) and expects it to be zeroed. + +**Behavior**: +- Expects account to already exist with allocated space +- Expects all bytes to be zero (except discriminator) +- Does NOT create the account + +**Use case**: Two-phase initialization where space is allocated separately + +**Example**: +```rust +#[account(zero)] +pub pre_allocated: Account<'info, MyData>, +``` + +--- + +## 4. `mut` + +**Purpose**: Marks an account as mutable. + +**Behavior**: +- Enables writing to account data +- Required for any account that will be modified + +**Generated check**: +```rust +if !account.is_writable { + return Err(ErrorCode::ConstraintMut.into()); +} +``` + +**Example**: +```rust +#[account(mut)] +pub counter: Account<'info, Counter>, +``` + +--- + +## 5. `close` + +**Purpose**: Closes an account and transfers lamports to a destination. + +**Behavior**: +- Sets account data to zero +- Sets discriminator to `CLOSED_ACCOUNT_DISCRIMINATOR` +- Transfers all lamports to specified account + +**Example**: +```rust +#[account(mut, close = user)] +pub data_account: Account<'info, MyData>, + +#[account(mut)] +pub user: Signer<'info>, +``` + +**Generated code**: +```rust +data_account.close(user.to_account_info())?; +``` + +--- + +## 6. `realloc` + +**Purpose**: Resizes an account's data allocation. + +**Required companions**: `realloc::payer`, `realloc::zero` + +**Behavior**: +- If growing: transfers lamports from payer for additional rent +- If shrinking: returns excess lamports to payer +- `realloc::zero = true` zeros new bytes when growing + +**Example**: +```rust +#[account( + mut, + realloc = 8 + new_size, + realloc::payer = user, + realloc::zero = true +)] +pub dynamic_account: Account<'info, DynamicData>, +``` + +--- + +## 7. `signer` + +**Purpose**: Asserts that an account has signed the transaction. + +**Behavior**: +- Checks `account_info.is_signer == true` + +**Note**: The `Signer<'info>` type automatically enforces this. + +**Example**: +```rust +#[account(signer)] +pub authority: AccountInfo<'info>, +``` + +--- + +## 8. `has_one` + +**Purpose**: Validates that a field in the account matches another account in the instruction. + +**Behavior**: +- Reads a `Pubkey` field from the account +- Compares it to another account's key + +**Example**: +```rust +#[account(has_one = authority)] +pub config: Account<'info, Config>, + +pub authority: Signer<'info>, +``` + +**Generated check**: +```rust +if config.authority != authority.key() { + return Err(ErrorCode::ConstraintHasOne.into()); +} +``` + +--- + +## 9. `owner` + +**Purpose**: Validates the owner program of an account. + +**Example**: +```rust +#[account(owner = token::ID)] +pub token_account: AccountInfo<'info>, +``` + +**Generated check**: +```rust +if *account.owner != expected_owner { + return Err(ErrorCode::ConstraintOwner.into()); +} +``` + +--- + +## 10. `address` + +**Purpose**: Validates that an account's address matches an expected value. + +**Example**: +```rust +#[account(address = MY_CONSTANT_PUBKEY)] +pub specific_account: AccountInfo<'info>, +``` + +**Generated check**: +```rust +if account.key() != expected_address { + return Err(ErrorCode::ConstraintAddress.into()); +} +``` + +--- + +## 11. `executable` + +**Purpose**: Validates that an account is an executable program. + +**Example**: +```rust +#[account(executable)] +pub some_program: AccountInfo<'info>, +``` + +**Generated check**: +```rust +if !account.executable { + return Err(ErrorCode::ConstraintExecutable.into()); +} +``` + +--- + +## 12. `seeds` + `bump` + +**Purpose**: Derives and validates a PDA (Program Derived Address). + +**Behavior**: +- Derives PDA from provided seeds +- Validates account address matches derived PDA +- Can be combined with `init` to create PDAs + +**Example**: +```rust +#[account( + init, + seeds = [b"config", user.key().as_ref()], + bump, + payer = user, + space = 8 + 64 +)] +pub user_config: Account<'info, UserConfig>, +``` + +**With explicit bump**: +```rust +#[account( + seeds = [b"config", user.key().as_ref()], + bump = user_config.bump +)] +pub user_config: Account<'info, UserConfig>, +``` + +--- + +## 13. `rent_exempt` + +**Purpose**: Controls rent exemption behavior. + +**Options**: +- `rent_exempt = enforce`: Account must be rent-exempt +- `rent_exempt = skip`: Skip rent exemption check + +**Example**: +```rust +#[account(rent_exempt = enforce)] +pub my_account: Account<'info, MyData>, +``` + +--- + +## 14. `constraint` + +**Purpose**: Arbitrary boolean constraint with custom error. + +**Behavior**: +- Evaluates a boolean expression +- Fails with custom error if false + +**Example**: +```rust +#[account( + constraint = counter.count < 100 @ MyError::CounterOverflow +)] +pub counter: Account<'info, Counter>, +``` + +**Generated check**: +```rust +if !(counter.count < 100) { + return Err(MyError::CounterOverflow.into()); +} +``` + +--- + +## Constraint Execution Order + +When multiple constraints are specified, they execute in this order: + +1. **Signer checks** (`signer`) +2. **Owner checks** (`owner`) +3. **Address checks** (`address`) +4. **Executable checks** (`executable`) +5. **Seeds derivation** (`seeds`, `bump`) +6. **Account creation** (`init`, `init_if_needed`, `zero`) +7. **Mutability checks** (`mut`) +8. **Rent exemption** (`rent_exempt`) +9. **Has-one relationships** (`has_one`) +10. **Custom constraints** (`constraint`) +11. **Reallocation** (`realloc`) +12. **Account closing** (`close`) - happens in exit handler + +--- + +## Combined Example + +```rust +#[derive(Accounts)] +#[instruction(new_authority: Pubkey)] +pub struct TransferAuthority<'info> { + #[account( + mut, + seeds = [b"config"], + bump = config.bump, + has_one = authority, + constraint = new_authority != Pubkey::default() @ MyError::InvalidAuthority + )] + pub config: Account<'info, Config>, + + pub authority: Signer<'info>, +} +``` + +This validates: +1. Account is writable +2. PDA matches expected derivation +3. `config.authority == authority.key()` +4. New authority is not the default pubkey diff --git a/sdk-libs/macros/docs/features/anchor-spl-features.md b/sdk-libs/macros/docs/features/anchor-spl-features.md new file mode 100644 index 0000000000..97ef13bfa2 --- /dev/null +++ b/sdk-libs/macros/docs/features/anchor-spl-features.md @@ -0,0 +1,411 @@ +# Anchor SPL Features + +This document covers the 12 SPL-specific features available in Anchor through the `anchor-spl` crate for working with tokens and mints. + +## Overview + +Anchor SPL provides typed wrappers and constraints for SPL Token and Token-2022 programs. These features enable type-safe token account and mint initialization with validation. + +```rust +use anchor_spl::token::{Token, TokenAccount, Mint}; +use anchor_spl::token_interface::{TokenInterface, TokenAccount as InterfaceTokenAccount}; +``` + +--- + +## Token Account Constraints + +### 1. `token::mint` + +**Purpose**: Validates that a token account's mint matches the specified mint. + +**Behavior**: +- Reads the `mint` field from the token account +- Compares against the specified mint account's key + +**Example**: +```rust +#[account(token::mint = usdc_mint)] +pub user_token_account: Account<'info, TokenAccount>, + +pub usdc_mint: Account<'info, Mint>, +``` + +**Generated check**: +```rust +if user_token_account.mint != usdc_mint.key() { + return Err(ErrorCode::ConstraintTokenMint.into()); +} +``` + +--- + +### 2. `token::authority` + +**Purpose**: Validates that a token account's authority (owner) matches the specified account. + +**Behavior**: +- Reads the `owner` field from the token account +- Compares against the specified authority's key + +**Example**: +```rust +#[account(token::authority = user)] +pub user_token_account: Account<'info, TokenAccount>, + +pub user: Signer<'info>, +``` + +**Generated check**: +```rust +if user_token_account.owner != user.key() { + return Err(ErrorCode::ConstraintTokenOwner.into()); +} +``` + +--- + +### 3. `token::token_program` + +**Purpose**: Specifies which token program owns this account (SPL Token or Token-2022). + +**Use case**: When working with Token-2022 accounts or when the program could be either. + +**Example**: +```rust +#[account( + init, + payer = user, + token::mint = mint, + token::authority = user, + token::token_program = token_program +)] +pub token_account: InterfaceAccount<'info, TokenAccount>, + +pub token_program: Interface<'info, TokenInterface>, +``` + +--- + +## Mint Constraints + +### 4. `mint::authority` + +**Purpose**: Sets or validates the mint authority. + +**Behavior when initializing**: Sets the mint authority to the specified account +**Behavior when validating**: Checks mint's `mint_authority` matches + +**Example** (initialization): +```rust +#[account( + init, + payer = user, + mint::decimals = 6, + mint::authority = authority +)] +pub new_mint: Account<'info, Mint>, + +pub authority: Signer<'info>, +``` + +--- + +### 5. `mint::decimals` + +**Purpose**: Sets or validates the mint's decimal places. + +**Behavior when initializing**: Sets decimals to specified value +**Behavior when validating**: Checks mint's `decimals` matches + +**Example**: +```rust +#[account( + init, + payer = user, + mint::decimals = 9, + mint::authority = authority +)] +pub new_mint: Account<'info, Mint>, +``` + +--- + +### 6. `mint::freeze_authority` + +**Purpose**: Sets or validates the freeze authority for a mint. + +**Behavior**: Optional authority that can freeze token accounts + +**Example**: +```rust +#[account( + init, + payer = user, + mint::decimals = 6, + mint::authority = authority, + mint::freeze_authority = freeze_authority +)] +pub new_mint: Account<'info, Mint>, + +pub freeze_authority: AccountInfo<'info>, +``` + +--- + +## Associated Token Account Constraints + +### 7. `associated_token::mint` + +**Purpose**: Specifies the mint for an associated token account derivation. + +**Behavior**: Used in PDA derivation: `[user.key, token_program.key, mint.key]` + +**Example**: +```rust +#[account( + init, + payer = user, + associated_token::mint = mint, + associated_token::authority = user +)] +pub user_ata: Account<'info, TokenAccount>, +``` + +--- + +### 8. `associated_token::authority` + +**Purpose**: Specifies the authority (wallet) for an associated token account. + +**Behavior**: The wallet whose ATA this is + +**Example**: +```rust +#[account( + associated_token::mint = mint, + associated_token::authority = wallet +)] +pub wallet_ata: Account<'info, TokenAccount>, + +pub wallet: SystemAccount<'info>, +pub mint: Account<'info, Mint>, +``` + +--- + +### 9. `associated_token::token_program` + +**Purpose**: Specifies the token program for ATA derivation. + +**Use case**: When working with Token-2022 associated token accounts + +**Example**: +```rust +#[account( + init, + payer = user, + associated_token::mint = mint, + associated_token::authority = user, + associated_token::token_program = token_program +)] +pub user_ata: InterfaceAccount<'info, TokenAccount>, + +pub token_program: Interface<'info, TokenInterface>, +pub associated_token_program: Program<'info, AssociatedToken>, +``` + +--- + +## Interface Types + +### 10. `InterfaceAccount<'info, T>` + +**Purpose**: Account wrapper that works with both SPL Token and Token-2022. + +**Behavior**: +- Accepts accounts from either token program +- Validates based on the interface, not specific program + +**Example**: +```rust +use anchor_spl::token_interface::TokenAccount; + +#[derive(Accounts)] +pub struct TransferTokens<'info> { + #[account(mut)] + pub from: InterfaceAccount<'info, TokenAccount>, + #[account(mut)] + pub to: InterfaceAccount<'info, TokenAccount>, +} +``` + +--- + +### 11. `TokenInterface` + +**Purpose**: Interface type that accepts either SPL Token or Token-2022 program. + +**Example**: +```rust +use anchor_spl::token_interface::TokenInterface; + +#[derive(Accounts)] +pub struct TokenOp<'info> { + pub token_program: Interface<'info, TokenInterface>, +} +``` + +**Generated validation**: +```rust +// Accepts either: +// - spl_token::ID +// - spl_token_2022::ID +``` + +--- + +## Token-2022 Extensions + +### 12. Token-2022 Extension Support + +**Purpose**: Support for Token-2022 extension features. + +Anchor SPL supports these Token-2022 extensions through the interface types: + +| Extension | Description | +|-----------|-------------| +| Transfer Fee | Automatic fee on transfers | +| Interest-Bearing | Accruing interest on balance | +| Non-Transferable | Soul-bound tokens | +| Permanent Delegate | Irrevocable delegate authority | +| Transfer Hook | Custom transfer logic | +| Metadata | On-chain token metadata | +| Confidential Transfers | ZK-based private transfers | +| Default Account State | Default frozen/unfrozen | +| CPI Guard | Prevents CPI-based attacks | +| Immutable Owner | Cannot change token account owner | +| Memo Required | Requires memo on transfers | +| Close Authority | Required authority to close | + +**Example with extensions**: +```rust +use anchor_spl::token_2022::Token2022; +use anchor_spl::token_interface::{ + Mint as InterfaceMint, + TokenAccount as InterfaceTokenAccount, +}; + +#[derive(Accounts)] +pub struct Token2022Op<'info> { + #[account( + mut, + token::mint = mint, + token::authority = owner, + token::token_program = token_program + )] + pub token_account: InterfaceAccount<'info, InterfaceTokenAccount>, + + pub mint: InterfaceAccount<'info, InterfaceMint>, + pub owner: Signer<'info>, + pub token_program: Program<'info, Token2022>, +} +``` + +--- + +## Complete Initialization Example + +```rust +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; + +#[derive(Accounts)] +pub struct InitializeToken<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + /// The mint to create + #[account( + init, + payer = payer, + mint::decimals = 6, + mint::authority = payer, + mint::freeze_authority = payer, + )] + pub mint: InterfaceAccount<'info, Mint>, + + /// The payer's associated token account + #[account( + init, + payer = payer, + associated_token::mint = mint, + associated_token::authority = payer, + associated_token::token_program = token_program, + )] + pub token_account: InterfaceAccount<'info, TokenAccount>, + + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, +} +``` + +--- + +## Constraint Combinations + +### Token Account with All Constraints + +```rust +#[account( + mut, + token::mint = mint, + token::authority = owner, + token::token_program = token_program, + constraint = token_account.amount >= required_amount @ MyError::InsufficientBalance +)] +pub token_account: InterfaceAccount<'info, TokenAccount>, +``` + +### Mint with All Constraints + +```rust +#[account( + init, + payer = payer, + mint::decimals = decimals, + mint::authority = mint_authority, + mint::freeze_authority = freeze_authority, + mint::token_program = token_program, +)] +pub mint: InterfaceAccount<'info, Mint>, +``` + +--- + +## CPI Helpers + +Anchor SPL also provides CPI helpers for token operations: + +```rust +use anchor_spl::token_interface::{transfer_checked, TransferChecked}; + +// Transfer tokens +transfer_checked( + CpiContext::new( + token_program.to_account_info(), + TransferChecked { + from: from_account.to_account_info(), + mint: mint.to_account_info(), + to: to_account.to_account_info(), + authority: authority.to_account_info(), + }, + ), + amount, + decimals, +)?; +``` diff --git a/sdk-libs/macros/docs/features/comparison.md b/sdk-libs/macros/docs/features/comparison.md new file mode 100644 index 0000000000..b6775f68e6 --- /dev/null +++ b/sdk-libs/macros/docs/features/comparison.md @@ -0,0 +1,259 @@ +# Feature Comparison: Anchor vs Light Protocol RentFree + +This document provides a comprehensive comparison between Anchor's account macros, Anchor SPL features, and Light Protocol's rentfree macro system. + +## Account Initialization Methods + +| Feature | Anchor | Anchor SPL | Light RentFree | +|---------|--------|------------|----------------| +| Create account | `init` | `init` | `init` (same) | +| Idempotent create | `init_if_needed` | `init_if_needed` | `init_if_needed` + compression check | +| Pre-allocated | `zero` | - | `zero` (same) | +| PDA creation | `seeds + bump + init` | `seeds + bump + init` | `seeds + bump + init` + address registration | +| Token account | - | `token::*` | `#[rentfree_token]` | +| Mint creation | - | `mint::*` | `#[light_mint]` | +| ATA creation | - | `associated_token::*` | Via `light_pre_init()` | + +## Constraint Types Matrix + +| Constraint | Anchor | Anchor SPL | Light RentFree | Notes | +|------------|--------|------------|----------------|-------| +| `mut` | Yes | Yes | Yes | Identical | +| `signer` | Yes | Yes | Yes | Identical | +| `close` | Yes | Yes | Yes + compression cleanup | Extended | +| `realloc` | Yes | Yes | Limited | Compression affects this | +| `has_one` | Yes | Yes | Yes | Identical | +| `owner` | Yes | Yes | Yes + compression owner | Extended | +| `address` | Yes | Yes | Yes | Identical | +| `executable` | Yes | Yes | Yes | Identical | +| `seeds + bump` | Yes | Yes | Yes + address derivation | Extended | +| `rent_exempt` | Yes | Yes | N/A | Rent-free by design | +| `constraint` | Yes | Yes | Yes | Identical | +| `token::mint` | - | Yes | Via `#[rentfree_token]` | Different syntax | +| `token::authority` | - | Yes | Via `#[rentfree_token]` | Different syntax | +| `mint::decimals` | - | Yes | Via `#[light_mint]` | Different syntax | +| `mint::authority` | - | Yes | Via `#[light_mint]` | Different syntax | +| `compression_info` | - | - | Yes | RentFree only | +| `compress_as` | - | - | Yes | RentFree only | + +## Execution Lifecycle Phases + +### Phase Comparison + +| Phase | Anchor | Light RentFree | +|-------|--------|----------------| +| 1. Extract AccountInfo | `try_accounts()` | `try_accounts()` | +| 2. System CPI (create) | `try_accounts()` | `try_accounts()` | +| 3. Token/Mint CPI | `try_accounts()` | `light_pre_init()` | +| 4. Deserialize | `try_accounts()` | `try_accounts()` | +| 5. Address registration | - | `light_pre_init()` | +| 6. Instruction handler | After `try_accounts()` | After `light_pre_init()` | +| 7. Compression finalize | - | `light_finalize()` | +| 8. Exit (close, etc.) | `exit()` | `exit()` + compression | + +### Visual Flow + +``` +ANCHOR: +================================================================================ +try_accounts() ─────────────────────────────────────────────> handler() -> exit() + │ + ├─ Extract AccountInfo + ├─ System CPI (init) + ├─ Token CPI (mint::*, token::*) + └─ Deserialize +================================================================================ + +LIGHT RENTFREE: +================================================================================ +try_accounts() ───> light_pre_init() ───> handler() ───> light_finalize() -> exit() + │ │ │ + ├─ Extract ├─ Register address ├─ Serialize state + ├─ System CPI ├─ Compressed mint CPI ├─ Create Merkle leaf + └─ Deserialize └─ Compression setup └─ Update tree +================================================================================ +``` + +## Deserialization Behavior + +| Scenario | Anchor | Light RentFree | +|----------|--------|----------------| +| New account (init) | Borsh deserialize after CPI | Borsh deserialize after CPI | +| Existing account | Borsh deserialize | Borsh deserialize | +| Compressed account | - | Merkle proof verify + deserialize | +| Mixed state | - | Check `compression_info.is_compressed` | +| Token account | SPL token layout | SPL token layout + compression_info | +| Mint account | SPL mint layout | SPL mint layout + compressed mint link | + +## Equivalence Mapping + +### Anchor -> RentFree Equivalents + +| Anchor Pattern | RentFree Equivalent | +|----------------|---------------------| +| `Account<'info, T>` | `Account<'info, T>` (with RentFree derive) | +| `#[account(init)]` | `#[account(init)]` + compression hooks | +| `#[account(init, token::mint = m)]` | `#[rentfree_token]` + `#[light_mint]` | +| `Program<'info, Token>` | `Program<'info, CompressedToken>` | +| `Account<'info, Mint>` | `UncheckedAccount<'info>` + `#[light_mint]` | +| `Account<'info, TokenAccount>` | `Account<'info, T>` with `#[rentfree_token]` | +| `close = destination` | `close = destination` + compression cleanup | +| Manual token CPI | Auto-generated in `light_pre_init()` | + +### Type Mapping + +| Anchor SPL Type | Light RentFree Type | +|-----------------|---------------------| +| `Mint` | `UncheckedAccount` (during init) | +| `TokenAccount` | Custom struct with `#[rentfree_token]` | +| `Token` program | `CompressedToken` program | +| `TokenInterface` | Not yet supported | +| `InterfaceAccount` | Not yet supported | + +## CPI Injection Patterns + +### Anchor Token CPI +```rust +// Anchor: CPI happens in try_accounts() during init +#[account( + init, + payer = user, + mint::decimals = 6, + mint::authority = user +)] +pub mint: Account<'info, Mint>, +``` + +### RentFree Token CPI +```rust +// RentFree: CPI happens in light_pre_init() after try_accounts() +/// CHECK: Created in light_pre_init +#[account(mut)] +#[light_mint(decimals = 6, authority = user)] +pub mint: UncheckedAccount<'info>, +``` + +### Why the Difference? + +1. **Anchor**: Mint exists during `try_accounts()`, so can use typed `Account<'info, Mint>` +2. **RentFree**: Mint created AFTER `try_accounts()`, so must use `UncheckedAccount` + +``` +Anchor timeline: +[System create] -> [Token init_mint] -> [Deserialize as Mint] -> [Handler] + ↑ + Typed access OK + +RentFree timeline: +[System create] -> [Deserialize as Unchecked] -> [light_pre_init: create compressed mint] -> [Handler] + ↑ ↑ + No type yet Compression happens here +``` + +## Data Struct Requirements + +| Requirement | Anchor | Light RentFree | +|-------------|--------|----------------| +| Discriminator | Auto (8 bytes) | Auto (8 bytes) | +| Borsh derive | Required | Required | +| Space calculation | Manual or `InitSpace` | Manual or `InitSpace` | +| CompressionInfo field | - | Required for compressible | +| compress_as attributes | - | Optional per field | +| Pack/Unpack traits | - | Generated by `#[derive(RentFree)]` | + +### Anchor Data Struct +```rust +#[account] +pub struct MyData { + pub owner: Pubkey, + pub value: u64, +} +// Space: 8 (discriminator) + 32 + 8 = 48 +``` + +### RentFree Data Struct +```rust +#[derive(RentFree, Compressible, HasCompressionInfo)] +#[rentfree] +pub struct MyData { + #[compress_as(pubkey)] + pub owner: Pubkey, + pub value: u64, + #[compression_info] + pub compression_info: CompressionInfo, +} +// Space: 8 (discriminator) + 32 + 8 + CompressionInfo::SIZE +``` + +## Limitations Comparison + +| Limitation | Anchor | Anchor SPL | Light RentFree | +|------------|--------|------------|----------------| +| Rent cost | Full rent | Full rent | Zero rent (compressed) | +| Account size limit | 10MB | 10MB | Effectively unlimited | +| Realloc support | Full | Full | Limited (compression boundary) | +| Interface accounts | Full | Full | Limited | +| Token-2022 | Full | Full | Partial | +| Cross-program composability | Full | Full | Requires proof | +| Immediate reads | Yes | Yes | Requires decompression or proof | +| Atomic updates | Yes | Yes | Yes (within tx) | +| Program complexity | Low | Low | Higher (proofs, trees) | +| Client complexity | Low | Low | Higher (proof generation) | + +## Migration Path + +### From Anchor to RentFree + +1. **Add derives**: Add `RentFree`, `Compressible`, `HasCompressionInfo` +2. **Add compression_info**: Add field to data structs +3. **Add compress_as**: Annotate fields for hashing +4. **Update program attribute**: Add `#[rentfree_program]` +5. **Add Light accounts**: Include protocol programs in accounts struct +6. **Update token handling**: Convert `mint::*` to `#[light_mint]` + +### Minimal Changes Example + +**Before (Anchor)**: +```rust +#[account] +pub struct Counter { + pub count: u64, +} +``` + +**After (RentFree)**: +```rust +#[derive(RentFree, Compressible, HasCompressionInfo)] +#[rentfree] +pub struct Counter { + pub count: u64, + #[compression_info] + pub compression_info: CompressionInfo, +} +``` + +## When to Use Each + +| Use Case | Recommended | +|----------|-------------| +| Standard Solana accounts | Anchor | +| SPL tokens (small scale) | Anchor SPL | +| High-volume token distribution | Light RentFree | +| Gaming (many player states) | Light RentFree | +| DeFi (composability critical) | Anchor + selective RentFree | +| NFT collections (large) | Light RentFree | +| DAO governance | Anchor (composability) | +| Airdrop campaigns | Light RentFree (cost) | +| Real-time trading | Anchor (speed) | +| Data archival | Light RentFree (cost) | + +## Summary + +| Aspect | Anchor | Light RentFree | +|--------|--------|----------------| +| **Primary benefit** | Simplicity, composability | Zero rent, scalability | +| **Learning curve** | Lower | Higher | +| **Runtime cost** | Rent + compute | Proof compute + no rent | +| **Best for** | General Solana dev | High-scale applications | +| **Ecosystem maturity** | Very mature | Growing | +| **Token support** | Full SPL + Token-2022 | Growing (SPL focus) | diff --git a/sdk-libs/macros/docs/features/rentfree-features.md b/sdk-libs/macros/docs/features/rentfree-features.md new file mode 100644 index 0000000000..23adfbf921 --- /dev/null +++ b/sdk-libs/macros/docs/features/rentfree-features.md @@ -0,0 +1,499 @@ +# Light Protocol RentFree Features + +This document covers the 17 features available in Light Protocol's rentfree macro system for creating compressed (rent-free) accounts and tokens. + +## Overview + +Light Protocol's rentfree macros enable developers to create compressed accounts that store data off-chain in Merkle trees while maintaining full Solana composability. These macros work alongside or as replacements for Anchor's account macros. + +```rust +use light_sdk::compressible::CompressionInfo; +use light_sdk_macros::{RentFree, Compressible, HasCompressionInfo}; + +#[derive(RentFree, Compressible, HasCompressionInfo)] +#[rentfree] +pub struct MyAccount { + pub data: u64, + #[compression_info] + pub compression_info: CompressionInfo, +} +``` + +--- + +## Account-Level Macros + +### 1. `#[derive(RentFree)]` + +**Purpose**: Generates the core traits needed for a compressible account. + +**Generates**: +- Serialization/deserialization implementations +- Account discriminator handling +- Pack/unpack logic for Solana accounts + +**Example**: +```rust +#[derive(RentFree)] +pub struct UserProfile { + pub name: [u8; 32], + pub score: u64, +} +``` + +--- + +### 2. `#[rentfree]` + +**Purpose**: Attribute for account structs that marks fields and configures compression behavior. + +**Supported field attributes**: +- `#[compression_info]` - Marks the CompressionInfo field +- `#[compress_as(...)]` - Specifies how to hash a field + +**Example**: +```rust +#[derive(RentFree)] +#[rentfree] +pub struct GameState { + #[compress_as(pubkey)] + pub player: Pubkey, + pub level: u8, + #[compression_info] + pub compression_info: CompressionInfo, +} +``` + +--- + +### 3. `#[rentfree_token]` + +**Purpose**: Marks an account as a token account that can be compressed/decompressed. + +**Behavior**: +- Generates token-specific pack/unpack implementations +- Integrates with compressed token program +- Handles token account state serialization + +**Example**: +```rust +#[derive(RentFree)] +#[rentfree_token] +pub struct MyTokenAccount { + pub mint: Pubkey, + pub owner: Pubkey, + pub amount: u64, + #[compression_info] + pub compression_info: CompressionInfo, +} +``` + +--- + +### 4. `#[light_mint]` + +**Purpose**: Creates a compressed mint alongside an on-chain mint PDA. + +**Behavior**: +- Generates mint initialization in `light_pre_init()` +- Creates compressed mint via CPI to compressed token program +- Links on-chain mint to compressed representation + +**Key insight**: Unlike Anchor's `mint::*` which runs during `try_accounts()`, `#[light_mint]` runs in `light_pre_init()` AFTER account deserialization. + +**Example**: +```rust +#[derive(Accounts)] +pub struct CreateMint<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + /// CHECK: Initialized in light_pre_init + #[account(mut)] + #[light_mint( + decimals = 6, + authority = payer, + freeze_authority = payer + )] + pub mint: UncheckedAccount<'info>, + + pub compressed_token_program: Program<'info, CompressedToken>, + pub system_program: Program<'info, System>, +} +``` + +**Why UncheckedAccount**: The mint doesn't exist during `try_accounts()`. It's created later in `light_pre_init()`, so typed `Account<'info, Mint>` would fail deserialization. + +--- + +### 5. `#[rentfree_program]` + +**Purpose**: Program-level attribute that generates compression lifecycle hooks. + +**Generates**: +- `light_pre_init()` function for pre-instruction setup +- `light_finalize()` function for post-instruction cleanup +- Account registration with Light system program +- CPI builders for compression operations + +**Example**: +```rust +#[rentfree_program] +#[program] +pub mod my_program { + use super::*; + + pub fn create_profile(ctx: Context) -> Result<()> { + // Business logic here + // Compression handled automatically in lifecycle hooks + Ok(()) + } +} +``` + +--- + +## Lifecycle Hooks + +### 6. `LightPreInit` Trait / `light_pre_init()` + +**Purpose**: Hook that runs AFTER `try_accounts()` but BEFORE instruction handler. + +**Responsibilities**: +- Register compressed account addresses +- Create compressed mints +- Initialize compression infrastructure + +**Generated for instructions with compressible accounts**: +```rust +// Generated pseudo-code +impl<'info> LightPreInit<'info> for CreateProfile<'info> { + fn light_pre_init(&mut self, /* params */) -> Result<()> { + // 1. Derive compressed account address + // 2. Register with Light system program + // 3. Create compressed mint if #[light_mint] + Ok(()) + } +} +``` + +--- + +### 7. `LightFinalize` Trait / `light_finalize()` + +**Purpose**: Hook that runs AFTER instruction handler completes. + +**Responsibilities**: +- Serialize account state for compression +- Create/update compressed account entries +- Handle compression proofs + +**Example flow**: +``` +try_accounts() -> light_pre_init() -> handler() -> light_finalize() +``` + +--- + +## Data Struct Derive Macros + +### 8. `#[derive(Compressible)]` + +**Purpose**: Implements the `Compressible` trait for hashing account data into Merkle leaves. + +**Generates**: +- `to_compressed_data()` method +- Field-by-field hashing logic +- Poseidon hash tree construction + +**Example**: +```rust +#[derive(Compressible)] +pub struct ProfileData { + pub name: [u8; 32], + pub level: u8, +} +``` + +--- + +### 9. `#[derive(CompressiblePack)]` + +**Purpose**: Combines `Compressible` with serialization for storage. + +**Generates**: +- `Compressible` implementation +- Borsh serialization +- Pack/unpack for Solana account data + +**Example**: +```rust +#[derive(CompressiblePack)] +pub struct GameSession { + pub player: Pubkey, + pub score: u64, + pub completed: bool, +} +``` + +--- + +### 10. `#[derive(LightCompressible)]` + +**Purpose**: Full compression support including address derivation. + +**Generates**: +- All `Compressible` functionality +- Address derivation helpers +- Merkle tree integration + +**Example**: +```rust +#[derive(LightCompressible)] +pub struct CompressedUserData { + pub owner: Pubkey, + pub data: [u8; 64], +} +``` + +--- + +### 11. `#[derive(HasCompressionInfo)]` + +**Purpose**: Implements accessors for the `CompressionInfo` field. + +**Generates**: +- `compression_info()` getter +- `compression_info_mut()` mutable getter +- Field detection from `#[compression_info]` attribute + +**Example**: +```rust +#[derive(HasCompressionInfo)] +pub struct MyAccount { + pub data: u64, + #[compression_info] + pub compression_info: CompressionInfo, +} +``` + +--- + +### 12. `#[derive(CompressAs)]` + +**Purpose**: Derives hashing behavior based on `#[compress_as(...)]` field attributes. + +**Supported compress_as types**: +- `pubkey` - Hash as 32-byte pubkey +- `u64` / `u128` - Hash as integer +- `bytes` - Hash as raw bytes +- `array` - Hash array elements + +**Example**: +```rust +#[derive(CompressAs)] +pub struct MixedData { + #[compress_as(pubkey)] + pub owner: Pubkey, + #[compress_as(u64)] + pub amount: u64, + #[compress_as(bytes)] + pub metadata: [u8; 32], +} +``` + +--- + +## Infrastructure Detection + +### 13. Automatic Program Detection + +**Purpose**: Macros automatically detect required Light Protocol programs. + +**Detected programs**: +- `light_system_program` - Core compression logic +- `account_compression_program` - Merkle tree management +- `compressed_token_program` - Token compression (if using tokens) +- `registered_program_pda` - Program registration + +**Behavior**: If these accounts are present in the Accounts struct, the macros wire them into CPIs automatically. + +--- + +### 14. Account Validation Generation + +**Purpose**: Generates validation checks similar to Anchor constraints. + +**Generated validations**: +- Ownership checks for compressed accounts +- Address derivation verification +- Compression state validation + +**Example generated code**: +```rust +// Pseudo-code for generated validation +if account.compression_info.is_compressed { + verify_merkle_proof(&account, &proof)?; +} +``` + +--- + +### 15. CPI Context Generation + +**Purpose**: Automatically builds CPI contexts for Light Protocol operations. + +**Generated for**: +- `compress_account` CPI +- `decompress_account` CPI +- `create_compressed_mint` CPI +- `transfer_compressed` CPI + +**Example**: +```rust +// Generated CPI builder +fn build_compress_cpi<'info>( + accounts: &MyAccounts<'info>, + data: CompressedAccountData, +) -> CpiContext<'_, '_, '_, 'info, CompressAccount<'info>> { + // Auto-generated from account struct +} +``` + +--- + +### 16. Seed Parameter Structs + +**Purpose**: Generates structs for PDA seed management. + +**Behavior**: +- Extracts seed fields from account definitions +- Creates typed seed parameter structs +- Integrates with address derivation + +**Example**: +```rust +// Generated from: +// #[account(seeds = [b"profile", user.key().as_ref()])] + +pub struct ProfileSeeds { + pub user: Pubkey, +} + +impl ProfileSeeds { + pub fn to_seeds(&self) -> [&[u8]; 2] { + [b"profile", self.user.as_ref()] + } +} +``` + +--- + +### 17. Variant Enum Generation + +**Purpose**: Creates enum variants for instruction dispatch with compression support. + +**Behavior**: +- Generates instruction enum with compression variants +- Handles both compressed and on-chain paths +- Integrates with Anchor's instruction dispatch + +**Example**: +```rust +// Generated enum +pub enum MyProgramInstruction { + CreateProfile, + CreateProfileCompressed, // Compressed variant + UpdateProfile, + CompressProfile, // Compression instruction + DecompressProfile, // Decompression instruction +} +``` + +--- + +## Execution Flow Comparison + +### Anchor Standard Flow +``` +try_accounts() { + 1. Extract AccountInfo + 2. Create via system CPI (init) + 3. Init token/mint CPI + 4. Deserialize +} +// instruction handler +``` + +### Light RentFree Flow +``` +try_accounts() { + 1. Extract AccountInfo + 2. Create PDA via system CPI (if init) + 3. Deserialize +} +light_pre_init() { + 4. Register compressed address + 5. Create compressed mint CPI (if #[light_mint]) +} +// instruction handler +light_finalize() { + 6. Complete compression +} +``` + +--- + +## Complete Example + +```rust +use anchor_lang::prelude::*; +use light_sdk::compressible::CompressionInfo; +use light_sdk_macros::*; + +#[derive(RentFree, Compressible, HasCompressionInfo)] +#[rentfree] +pub struct UserProfile { + #[compress_as(pubkey)] + pub owner: Pubkey, + pub username: [u8; 32], + pub level: u8, + #[compression_info] + pub compression_info: CompressionInfo, +} + +#[rentfree_program] +#[program] +pub mod my_program { + use super::*; + + pub fn create_profile(ctx: Context, username: [u8; 32]) -> Result<()> { + let profile = &mut ctx.accounts.profile; + profile.owner = ctx.accounts.user.key(); + profile.username = username; + profile.level = 1; + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CreateProfile<'info> { + #[account(mut)] + pub user: Signer<'info>, + + #[account( + init, + payer = user, + space = 8 + UserProfile::SIZE, + seeds = [b"profile", user.key().as_ref()], + bump + )] + pub profile: Account<'info, UserProfile>, + + pub system_program: Program<'info, System>, + + // Light Protocol infrastructure (auto-detected) + pub light_system_program: Program<'info, LightSystem>, + pub account_compression_program: Program<'info, AccountCompression>, +} +``` diff --git a/sdk-libs/macros/src/rentfree/account/decompress_context.rs b/sdk-libs/macros/src/rentfree/account/decompress_context.rs index 31fd02aecf..0a9da002ea 100644 --- a/sdk-libs/macros/src/rentfree/account/decompress_context.rs +++ b/sdk-libs/macros/src/rentfree/account/decompress_context.rs @@ -95,7 +95,7 @@ pub fn generate_decompress_context_trait_impl( )?; } RentFreeAccountVariant::#variant_name { .. } => { - unreachable!("Unpacked variants should not be present during decompression"); + return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into()); } } }) @@ -175,7 +175,7 @@ pub fn generate_decompress_context_trait_impl( solana_msg::msg!("collect_pda_and_token: token {} done", i); } RentFreeAccountVariant::CTokenData(_) => { - unreachable!(); + return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into()); } } } diff --git a/sdk-libs/macros/src/rentfree/account/pack_unpack.rs b/sdk-libs/macros/src/rentfree/account/pack_unpack.rs index df59bc2fcf..181fe6ee2a 100644 --- a/sdk-libs/macros/src/rentfree/account/pack_unpack.rs +++ b/sdk-libs/macros/src/rentfree/account/pack_unpack.rs @@ -25,8 +25,8 @@ fn generate_with_packed_struct( packed_struct_name: &syn::Ident, fields: &syn::punctuated::Punctuated, ) -> Result { - let packed_fields = fields.iter().map(|field| { - let field_name = field.ident.as_ref().unwrap(); + let packed_fields = fields.iter().filter_map(|field| { + let field_name = field.ident.as_ref()?; let field_type = &field.ty; let packed_type = if is_pubkey_type(field_type) { @@ -35,7 +35,7 @@ fn generate_with_packed_struct( quote! { #field_type } }; - quote! { pub #field_name: #packed_type } + Some(quote! { pub #field_name: #packed_type }) }); let packed_struct = quote! { @@ -45,11 +45,11 @@ fn generate_with_packed_struct( } }; - let pack_field_assignments = fields.iter().map(|field| { - let field_name = field.ident.as_ref().unwrap(); + let pack_field_assignments = fields.iter().filter_map(|field| { + let field_name = field.ident.as_ref()?; let field_type = &field.ty; - if *field_name == "compression_info" { + Some(if *field_name == "compression_info" { quote! { #field_name: None } } else if is_pubkey_type(field_type) { // Use read-only since pubkey fields are references (owner, authority, etc.) @@ -59,7 +59,7 @@ fn generate_with_packed_struct( quote! { #field_name: self.#field_name } } else { quote! { #field_name: self.#field_name.clone() } - } + }) }); let pack_impl = quote! { @@ -67,10 +67,10 @@ fn generate_with_packed_struct( type Packed = #packed_struct_name; #[inline(never)] - fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { - #packed_struct_name { + fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> std::result::Result { + Ok(#packed_struct_name { #(#pack_field_assignments,)* - } + }) } } }; @@ -94,17 +94,17 @@ fn generate_with_packed_struct( type Packed = Self; #[inline(never)] - fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { - self.clone() + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> std::result::Result { + Ok(self.clone()) } } }; - let unpack_field_assignments = fields.iter().map(|field| { - let field_name = field.ident.as_ref().unwrap(); + let unpack_field_assignments = fields.iter().filter_map(|field| { + let field_name = field.ident.as_ref()?; let field_type = &field.ty; - if *field_name == "compression_info" { + Some(if *field_name == "compression_info" { quote! { #field_name: None } } else if is_pubkey_type(field_type) { quote! { @@ -114,7 +114,7 @@ fn generate_with_packed_struct( quote! { #field_name: self.#field_name } } else { quote! { #field_name: self.#field_name.clone() } - } + }) }); let unpack_impl_packed = quote! { @@ -158,8 +158,8 @@ fn generate_identity_pack_unpack(struct_name: &syn::Ident) -> Result Self::Packed { - self.clone() + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> std::result::Result { + Ok(self.clone()) } } }; diff --git a/sdk-libs/macros/src/rentfree/account/seed_extraction.rs b/sdk-libs/macros/src/rentfree/account/seed_extraction.rs index fc6150f614..1d65b1fad8 100644 --- a/sdk-libs/macros/src/rentfree/account/seed_extraction.rs +++ b/sdk-libs/macros/src/rentfree/account/seed_extraction.rs @@ -497,18 +497,15 @@ pub fn classify_seed_expr(expr: &Expr) -> syn::Result { // Reference like &account.key() Expr::Reference(r) => classify_seed_expr(&r.expr), - // Field access like params.owner - direct field reference + // Field access like params.owner or params.nested.owner - direct field reference Expr::Field(field) => { if let syn::Member::Named(field_name) = &field.member { - if let Expr::Path(path) = &*field.base { - if let Some(base_ident) = path.path.get_ident() { - if base_ident == "params" { - return Ok(ClassifiedSeed::DataField { - field_name: field_name.clone(), - conversion: None, - }); - } - } + // Check if root of the expression is "params" + if is_params_rooted(&field.base) { + return Ok(ClassifiedSeed::DataField { + field_name: field_name.clone(), + conversion: None, + }); } // ctx.field or account.field - treat as ctx account return Ok(ClassifiedSeed::CtxAccount(field_name.clone())); @@ -541,6 +538,38 @@ pub fn classify_seed_expr(expr: &Expr) -> syn::Result { Ok(ClassifiedSeed::FunctionCall { func, ctx_args }) } + // Index expression - handles two cases: + // 1. b"literal"[..] - converts [u8; N] to &[u8] + // 2. params.arrays[2] - array indexing on params field + Expr::Index(idx) => { + // Case 1: Check if the index is a full range (..) on byte literal + if let Expr::Range(range) = &*idx.index { + if range.start.is_none() && range.end.is_none() { + // This is a full range [..], now check if expr is a byte string literal + if let Expr::Lit(lit) = &*idx.expr { + if let syn::Lit::ByteStr(bs) = &lit.lit { + return Ok(ClassifiedSeed::Literal(bs.value())); + } + } + } + } + + // Case 2: Array indexing on params field like params.arrays[2] + if is_params_rooted(&idx.expr) { + if let Some(field_name) = extract_terminal_field(&idx.expr) { + return Ok(ClassifiedSeed::DataField { + field_name, + conversion: None, + }); + } + } + + Err(syn::Error::new_spanned( + expr, + format!("Unsupported index expression in seeds: {:?}", expr), + )) + } + _ => Err(syn::Error::new_spanned( expr, format!("Unsupported seed expression: {:?}", expr), @@ -550,48 +579,41 @@ pub fn classify_seed_expr(expr: &Expr) -> syn::Result { /// Classify a method call expression like account.key().as_ref() fn classify_method_call(mc: &syn::ExprMethodCall) -> syn::Result { - // Unwrap .as_ref() or .as_bytes() at the end - if mc.method == "as_ref" || mc.method == "as_bytes" { + // Unwrap .as_ref(), .as_bytes(), or .as_slice() at the end - these are terminal conversions + if mc.method == "as_ref" || mc.method == "as_bytes" || mc.method == "as_slice" { return classify_seed_expr(&mc.receiver); } - // Handle params.field.to_le_bytes() directly - if mc.method == "to_le_bytes" || mc.method == "to_be_bytes" { - if let Some((field_name, base)) = extract_params_field(&mc.receiver) { - if base == "params" { - return Ok(ClassifiedSeed::DataField { - field_name, - conversion: Some(mc.method.clone()), - }); - } + // Handle params.field.to_le_bytes() or params.nested.field.to_le_bytes() + if (mc.method == "to_le_bytes" || mc.method == "to_be_bytes") && is_params_rooted(&mc.receiver) + { + if let Some(field_name) = extract_terminal_field(&mc.receiver) { + return Ok(ClassifiedSeed::DataField { + field_name, + conversion: Some(mc.method.clone()), + }); } } // Handle account.key() if mc.method == "key" { if let Some(ident) = extract_terminal_ident(&mc.receiver, false) { - // Check if it's params.field or ctx.account - if let Expr::Field(field) = &*mc.receiver { - if let Expr::Path(path) = &*field.base { - if let Some(base_ident) = path.path.get_ident() { - if base_ident == "params" { - if let syn::Member::Named(field_name) = &field.member { - return Ok(ClassifiedSeed::DataField { - field_name: field_name.clone(), - conversion: None, - }); - } - } - } + // Check if it's rooted in params + if is_params_rooted(&mc.receiver) { + if let Some(field_name) = extract_terminal_field(&mc.receiver) { + return Ok(ClassifiedSeed::DataField { + field_name, + conversion: None, + }); } } return Ok(ClassifiedSeed::CtxAccount(ident)); } } - // params.field.as_ref() directly - if let Some((field_name, base)) = extract_params_field(&mc.receiver) { - if base == "params" { + // params.field or params.nested.field - check for params-rooted access + if is_params_rooted(&mc.receiver) { + if let Some(field_name) = extract_terminal_field(&mc.receiver) { return Ok(ClassifiedSeed::DataField { field_name, conversion: None, @@ -605,18 +627,38 @@ fn classify_method_call(mc: &syn::ExprMethodCall) -> syn::Result )) } -/// Extract field name from params.field or similar -fn extract_params_field(expr: &Expr) -> Option<(Ident, String)> { - if let Expr::Field(field) = expr { - if let syn::Member::Named(field_name) = &field.member { - if let Expr::Path(path) = &*field.base { - if let Some(base_ident) = path.path.get_ident() { - return Some((field_name.clone(), base_ident.to_string())); - } +/// Check if an expression is rooted in "params" (handles nested access like params.nested.field) +fn is_params_rooted(expr: &Expr) -> bool { + match expr { + Expr::Path(path) => path.path.get_ident().is_some_and(|ident| ident == "params"), + Expr::Field(field) => { + // Recursively check the base + is_params_rooted(&field.base) + } + Expr::Index(idx) => { + // For array indexing like params.arrays[2], check the base + is_params_rooted(&idx.expr) + } + _ => false, + } +} + +/// Extract the terminal field name from a nested field access (e.g., params.nested.owner -> owner) +fn extract_terminal_field(expr: &Expr) -> Option { + match expr { + Expr::Field(field) => { + if let syn::Member::Named(field_name) = &field.member { + Some(field_name.clone()) + } else { + None } } + Expr::Index(idx) => { + // For indexed access, get the field name from the base + extract_terminal_field(&idx.expr) + } + _ => None, } - None } /// Get data field names from classified seeds diff --git a/sdk-libs/macros/src/rentfree/account/traits.rs b/sdk-libs/macros/src/rentfree/account/traits.rs index f7b5413dad..0418be675d 100644 --- a/sdk-libs/macros/src/rentfree/account/traits.rs +++ b/sdk-libs/macros/src/rentfree/account/traits.rs @@ -69,20 +69,21 @@ fn validate_compression_info_field( fn generate_has_compression_info_impl(struct_name: &Ident) -> TokenStream { quote! { impl light_sdk::compressible::HasCompressionInfo for #struct_name { - fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { - self.compression_info.as_ref().expect("compression_info must be set") + fn compression_info(&self) -> std::result::Result<&light_sdk::compressible::CompressionInfo, solana_program_error::ProgramError> { + self.compression_info.as_ref().ok_or(light_sdk::error::LightSdkError::MissingCompressionInfo.into()) } - fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { - self.compression_info.as_mut().expect("compression_info must be set") + fn compression_info_mut(&mut self) -> std::result::Result<&mut light_sdk::compressible::CompressionInfo, solana_program_error::ProgramError> { + self.compression_info.as_mut().ok_or(light_sdk::error::LightSdkError::MissingCompressionInfo.into()) } fn compression_info_mut_opt(&mut self) -> &mut Option { &mut self.compression_info } - fn set_compression_info_none(&mut self) { + fn set_compression_info_none(&mut self) -> std::result::Result<(), solana_program_error::ProgramError> { self.compression_info = None; + Ok(()) } } } @@ -97,7 +98,9 @@ fn generate_compress_as_field_assignments( let mut field_assignments = Vec::new(); for field in fields { - let field_name = field.ident.as_ref().unwrap(); + let Some(field_name) = field.ident.as_ref() else { + continue; + }; let field_type = &field.ty; // Auto-skip compression_info field (handled separately in CompressAs impl) @@ -158,7 +161,9 @@ fn generate_size_fields(fields: &Punctuated) -> Vec) -> Vec TokenStream { quote! { impl light_sdk::account::Size for #struct_name { - fn size(&self) -> usize { + fn size(&self) -> std::result::Result { // Always allocate space for Some(CompressionInfo) since it will be set during decompression // CompressionInfo size: 1 byte (Option discriminant) + ::INIT_SPACE let compression_info_size = 1 + ::INIT_SPACE; - compression_info_size #(#size_fields)* + Ok(compression_info_size #(#size_fields)*) } } } diff --git a/sdk-libs/macros/src/rentfree/accounts/builder.rs b/sdk-libs/macros/src/rentfree/accounts/builder.rs index 14ba9d463c..8ddb9b0f8b 100644 --- a/sdk-libs/macros/src/rentfree/accounts/builder.rs +++ b/sdk-libs/macros/src/rentfree/accounts/builder.rs @@ -30,6 +30,20 @@ impl RentFreeBuilder { Ok(Self { parsed, infra }) } + /// Get the first instruction argument, returning an error if missing. + fn get_first_instruction_arg(&self) -> Result<&super::parse::InstructionArg, syn::Error> { + self.parsed + .instruction_args + .as_ref() + .and_then(|args| args.first()) + .ok_or_else(|| { + syn::Error::new_spanned( + &self.parsed.struct_name, + "Missing #[instruction(...)] attribute with at least one parameter", + ) + }) + } + /// Validate constraints (e.g., account count < 255). pub fn validate(&self) -> Result<(), syn::Error> { let total = self.parsed.rentfree_fields.len() + self.parsed.light_mint_fields.len(); @@ -98,21 +112,15 @@ impl RentFreeBuilder { /// 1. Write PDAs to CPI context /// 2. Invoke CreateMintsCpi with CPI context offset /// After this, Mints are "hot" and usable in instruction body - pub fn generate_pre_init_pdas_and_mints(&self) -> TokenStream { + pub fn generate_pre_init_pdas_and_mints(&self) -> Result { let (compress_blocks, new_addr_idents) = generate_pda_compress_blocks(&self.parsed.rentfree_fields); let rentfree_count = self.parsed.rentfree_fields.len() as u8; let pda_count = self.parsed.rentfree_fields.len(); // Get instruction param ident - let params_ident = &self - .parsed - .instruction_args - .as_ref() - .unwrap() - .first() - .unwrap() - .name; + let first_arg = self.get_first_instruction_arg()?; + let params_ident = &first_arg.name; // Get the first PDA's output tree index (for the state tree output queue) let first_pda_output_tree = &self.parsed.rentfree_fields[0].output_tree; @@ -127,7 +135,7 @@ impl RentFreeBuilder { let fee_payer = &self.infra.fee_payer; let compression_config = &self.infra.compression_config; - quote! { + Ok(quote! { // Build CPI accounts WITH CPI context for batching let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new_with_config( &self.#fee_payer, @@ -168,22 +176,16 @@ impl RentFreeBuilder { #mint_invocation Ok(true) - } + }) } /// Generate LightPreInit body for mints-only (no PDAs): /// Invoke CreateMintsCpi with decompress directly /// After this, Mints are "hot" and usable in instruction body - pub fn generate_pre_init_mints_only(&self) -> TokenStream { + pub fn generate_pre_init_mints_only(&self) -> Result { // Get instruction param ident - let params_ident = &self - .parsed - .instruction_args - .as_ref() - .unwrap() - .first() - .unwrap() - .name; + let first_arg = self.get_first_instruction_arg()?; + let params_ident = &first_arg.name; // Generate CreateMintsCpi invocation for all mints (no PDA context) let mints = &self.parsed.light_mint_fields; @@ -193,7 +195,7 @@ impl RentFreeBuilder { // Infrastructure field reference for quote! interpolation let fee_payer = &self.infra.fee_payer; - quote! { + Ok(quote! { // Build CPI accounts with CPI context enabled (mints use CPI context for batching) let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new_with_config( &self.#fee_payer, @@ -205,31 +207,25 @@ impl RentFreeBuilder { #mint_invocation Ok(true) - } + }) } /// Generate LightPreInit body for PDAs only (no mints) /// After this, compressed addresses are registered - pub fn generate_pre_init_pdas_only(&self) -> TokenStream { + pub fn generate_pre_init_pdas_only(&self) -> Result { let (compress_blocks, new_addr_idents) = generate_pda_compress_blocks(&self.parsed.rentfree_fields); let rentfree_count = self.parsed.rentfree_fields.len() as u8; // Get instruction param ident - let params_ident = &self - .parsed - .instruction_args - .as_ref() - .unwrap() - .first() - .unwrap() - .name; + let first_arg = self.get_first_instruction_arg()?; + let params_ident = &first_arg.name; // Infra field references let fee_payer = &self.infra.fee_payer; let compression_config = &self.infra.compression_config; - quote! { + Ok(quote! { // Build CPI accounts (no CPI context needed for PDAs-only) let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( &self.#fee_payer, @@ -258,25 +254,20 @@ impl RentFreeBuilder { .invoke(cpi_accounts)?; Ok(true) - } + }) } /// Generate LightPreInit trait implementation. - pub fn generate_pre_init_impl(&self, body: TokenStream) -> TokenStream { + pub fn generate_pre_init_impl(&self, body: TokenStream) -> Result { let struct_name = &self.parsed.struct_name; let (impl_generics, ty_generics, where_clause) = self.parsed.generics.split_for_impl(); - let first_arg = self - .parsed - .instruction_args - .as_ref() - .and_then(|args| args.first()) - .unwrap(); + let first_arg = self.get_first_instruction_arg()?; let params_type = &first_arg.ty; let params_ident = &first_arg.name; - quote! { + Ok(quote! { #[automatically_derived] impl #impl_generics light_sdk::compressible::LightPreInit<'info, #params_type> for #struct_name #ty_generics #where_clause { fn light_pre_init( @@ -288,25 +279,20 @@ impl RentFreeBuilder { #body } } - } + }) } /// Generate LightFinalize trait implementation. - pub fn generate_finalize_impl(&self, body: TokenStream) -> TokenStream { + pub fn generate_finalize_impl(&self, body: TokenStream) -> Result { let struct_name = &self.parsed.struct_name; let (impl_generics, ty_generics, where_clause) = self.parsed.generics.split_for_impl(); - let first_arg = self - .parsed - .instruction_args - .as_ref() - .and_then(|args| args.first()) - .unwrap(); + let first_arg = self.get_first_instruction_arg()?; let params_type = &first_arg.ty; let params_ident = &first_arg.name; - quote! { + Ok(quote! { #[automatically_derived] impl #impl_generics light_sdk::compressible::LightFinalize<'info, #params_type> for #struct_name #ty_generics #where_clause { fn light_finalize( @@ -319,6 +305,6 @@ impl RentFreeBuilder { #body } } - } + }) } } diff --git a/sdk-libs/macros/src/rentfree/accounts/derive.rs b/sdk-libs/macros/src/rentfree/accounts/derive.rs index 0de5ce2c34..a0537c7b24 100644 --- a/sdk-libs/macros/src/rentfree/accounts/derive.rs +++ b/sdk-libs/macros/src/rentfree/accounts/derive.rs @@ -35,18 +35,18 @@ pub(super) fn derive_rentfree(input: &DeriveInput) -> Result, /// Write top-up lamports for decompression (default: 0) pub write_top_up: Option, + // Metadata extension fields + /// Token name for TokenMetadata extension + pub name: Option, + /// Token symbol for TokenMetadata extension + pub symbol: Option, + /// Token URI for TokenMetadata extension + pub uri: Option, + /// Update authority field reference for TokenMetadata extension + pub update_authority: Option, + /// Additional metadata key-value pairs for TokenMetadata extension + pub additional_metadata: Option, } /// Arguments inside #[light_mint(...)] parsed by darling. /// /// Required fields (darling auto-validates): mint_signer, authority, decimals /// Optional fields: address_tree_info, freeze_authority, mint_seeds, authority_seeds, rent_payment, write_top_up +/// Metadata fields (all optional): name, symbol, uri, update_authority, additional_metadata #[derive(FromMeta)] struct LightMintArgs { /// The mint_signer field (AccountInfo that seeds the mint PDA) - REQUIRED @@ -82,6 +94,56 @@ struct LightMintArgs { /// Write top-up lamports for decompression #[darling(default)] write_top_up: Option, + // Metadata extension fields + /// Token name for TokenMetadata extension (expression yielding Vec) + #[darling(default)] + name: Option, + /// Token symbol for TokenMetadata extension (expression yielding Vec) + #[darling(default)] + symbol: Option, + /// Token URI for TokenMetadata extension (expression yielding Vec) + #[darling(default)] + uri: Option, + /// Update authority field reference for TokenMetadata extension + #[darling(default)] + update_authority: Option, + /// Additional metadata for TokenMetadata extension (expression yielding Option>) + #[darling(default)] + additional_metadata: Option, +} + +/// Validates TokenMetadata field requirements. +/// +/// Rules: +/// 1. `name`, `symbol`, `uri` must all be defined together or none +/// 2. `update_authority` and `additional_metadata` require `name`, `symbol`, `uri` +fn validate_metadata_fields(args: &LightMintArgs) -> Result<(), &'static str> { + let has_name = args.name.is_some(); + let has_symbol = args.symbol.is_some(); + let has_uri = args.uri.is_some(); + let has_update_authority = args.update_authority.is_some(); + let has_additional_metadata = args.additional_metadata.is_some(); + + let core_metadata_count = [has_name, has_symbol, has_uri] + .iter() + .filter(|&&x| x) + .count(); + + // Rule 1: name, symbol, uri must all be defined together or none + if core_metadata_count > 0 && core_metadata_count < 3 { + return Err( + "TokenMetadata requires all of `name`, `symbol`, and `uri` to be specified together", + ); + } + + // Rule 2: update_authority and additional_metadata require name, symbol, uri + if (has_update_authority || has_additional_metadata) && core_metadata_count == 0 { + return Err( + "`update_authority` and `additional_metadata` require `name`, `symbol`, and `uri` to also be specified", + ); + } + + Ok(()) } /// Parse #[light_mint(...)] attribute from a field. @@ -96,6 +158,9 @@ pub(super) fn parse_light_mint_attr( let args = LightMintArgs::from_meta(&attr.meta) .map_err(|e| syn::Error::new_spanned(attr, e.to_string()))?; + // Validate metadata fields + validate_metadata_fields(&args).map_err(|msg| syn::Error::new_spanned(attr, msg))?; + // address_tree_info defaults to params.create_accounts_proof.address_tree_info let address_tree_info = args.address_tree_info.map(Into::into).unwrap_or_else(|| { syn::parse_quote!(params.create_accounts_proof.address_tree_info) @@ -112,6 +177,12 @@ pub(super) fn parse_light_mint_attr( authority_seeds: args.authority_seeds.map(Into::into), rent_payment: args.rent_payment.map(Into::into), write_top_up: args.write_top_up.map(Into::into), + // Metadata extension fields + name: args.name.map(Into::into), + symbol: args.symbol.map(Into::into), + uri: args.uri.map(Into::into), + update_authority: args.update_authority, + additional_metadata: args.additional_metadata.map(Into::into), })); } } @@ -264,6 +335,7 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { let signer_key_ident = format_ident!("__mint_signer_key_{}", idx); let mint_seeds_ident = format_ident!("__mint_seeds_{}", idx); let authority_seeds_ident = format_ident!("__authority_seeds_{}", idx); + let token_metadata_ident = format_ident!("__mint_token_metadata_{}", idx); // Generate optional authority seeds binding let authority_seeds_binding = match authority_seeds { @@ -276,6 +348,39 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { }, }; + // Check if metadata is present (validation guarantees name/symbol/uri are all-or-nothing) + let has_metadata = mint.name.is_some(); + + // Generate token_metadata binding + let token_metadata_binding = if has_metadata { + // name, symbol, uri are guaranteed to be present by validation + let name_expr = mint.name.as_ref().map(|e| quote! { #e }).unwrap(); + let symbol_expr = mint.symbol.as_ref().map(|e| quote! { #e }).unwrap(); + let uri_expr = mint.uri.as_ref().map(|e| quote! { #e }).unwrap(); + let update_authority_expr = mint.update_authority.as_ref() + .map(|f| quote! { Some(self.#f.to_account_info().key.to_bytes().into()) }) + .unwrap_or_else(|| quote! { None }); + let additional_metadata_expr = mint.additional_metadata.as_ref() + .map(|e| quote! { #e }) + .unwrap_or_else(|| quote! { None }); + + quote! { + let #token_metadata_ident: Option = Some( + light_token_sdk::TokenMetadataInstructionData { + update_authority: #update_authority_expr, + name: #name_expr, + symbol: #symbol_expr, + uri: #uri_expr, + additional_metadata: #additional_metadata_expr, + } + ); + } + } else { + quote! { + let #token_metadata_ident: Option = None; + } + }; + quote! { // Mint #idx: derive PDA and build params let #signer_key_ident = *self.#mint_signer.to_account_info().key; @@ -283,6 +388,7 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { let #mint_seeds_ident: &[&[u8]] = #mint_seeds; #authority_seeds_binding + #token_metadata_binding let __tree_info = &#address_tree_info; @@ -297,6 +403,7 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { mint_seed_pubkey: #signer_key_ident, authority_seeds: #authority_seeds_ident, mint_signer_seeds: Some(#mint_seeds_ident), + token_metadata: #token_metadata_ident.as_ref(), }; } }) diff --git a/sdk-libs/macros/src/rentfree/program/compress.rs b/sdk-libs/macros/src/rentfree/program/compress.rs index 7df3703315..c2eead012d 100644 --- a/sdk-libs/macros/src/rentfree/program/compress.rs +++ b/sdk-libs/macros/src/rentfree/program/compress.rs @@ -1,4 +1,7 @@ //! Compress code generation. +//! +//! This module provides the `CompressBuilder` for generating compress instruction +//! code including context implementation, processor, entrypoint, and accounts struct. use proc_macro2::TokenStream; use quote::quote; @@ -8,222 +11,274 @@ use super::parsing::InstructionVariant; use crate::rentfree::shared_utils::qualify_type_with_crate; // ============================================================================= -// COMPRESS CONTEXT IMPL +// COMPRESS BUILDER // ============================================================================= -pub fn generate_compress_context_impl(account_types: Vec) -> Result { - let lifetime: syn::Lifetime = syn::parse_quote!('info); - - let compress_arms: Vec<_> = account_types.iter().map(|account_type| { - // Qualify with crate:: to ensure it's accessible from generated code - let name = qualify_type_with_crate(account_type); - quote! { - d if d == #name::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data().map_err(__anchor_to_program_error)?; - let mut account_data = #name::try_deserialize(&mut &data_borrow[..]) - .map_err(__anchor_to_program_error)?; - drop(data_borrow); - - let compressed_info = light_sdk::compressible::compress_account::prepare_account_for_compression::<#name>( - program_id, - account_info, - &mut account_data, - meta, - cpi_accounts, - &compression_config.address_space, - )?; - Ok(Some(compressed_info)) - } +/// Builder for generating compress instruction code. +/// +/// Encapsulates the account types and variant configuration needed to generate +/// all compress-related code: context implementation, processor function, +/// instruction entrypoint, and accounts struct. +pub(super) struct CompressBuilder { + /// Account types that can be compressed. + account_types: Vec, + /// The instruction variant (PdaOnly, TokenOnly, or Mixed). + variant: InstructionVariant, +} + +impl CompressBuilder { + /// Create a new CompressBuilder with the given account types and variant. + /// + /// # Arguments + /// * `account_types` - The account types that can be compressed + /// * `variant` - The instruction variant determining what gets generated + /// + /// # Returns + /// A new CompressBuilder instance + pub fn new(account_types: Vec, variant: InstructionVariant) -> Self { + Self { + account_types, + variant, } - }).collect(); - - Ok(syn::parse_quote! { - mod __compress_context_impl { - use super::*; - use light_sdk::LightDiscriminator; - use light_sdk::compressible::HasCompressionInfo; - - #[inline(always)] - fn __anchor_to_program_error>(e: E) -> solana_program_error::ProgramError { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - } + } - impl<#lifetime> light_sdk::compressible::CompressContext<#lifetime> for CompressAccountsIdempotent<#lifetime> { - fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &*self.fee_payer - } + // ------------------------------------------------------------------------- + // Query Methods + // ------------------------------------------------------------------------- - fn config(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &self.config - } + /// Returns true if this builder generates PDA compression code. + /// + /// This is true for `PdaOnly` and `Mixed` variants. + pub fn has_pdas(&self) -> bool { + matches!( + self.variant, + InstructionVariant::PdaOnly | InstructionVariant::Mixed + ) + } - fn rent_sponsor(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &self.rent_sponsor - } + /// Validate the builder configuration. + /// + /// Checks that: + /// - At least one account type is provided (for PDA variants) + /// - All account sizes are within the 800-byte limit + /// + /// # Returns + /// `Ok(())` if validation passes, or a `syn::Error` describing the issue. + pub fn validate(&self) -> Result<()> { + // For variants that include PDAs, require at least one account type + if self.has_pdas() && self.account_types.is_empty() { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + "CompressBuilder requires at least one account type for PDA compression", + )); + } + Ok(()) + } - fn compression_authority(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &self.compression_authority - } + // ------------------------------------------------------------------------- + // Code Generation Methods + // ------------------------------------------------------------------------- - fn compress_pda_account( - &self, - account_info: &solana_account_info::AccountInfo<#lifetime>, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'_, #lifetime>, - compression_config: &light_sdk::compressible::CompressibleConfig, - program_id: &solana_pubkey::Pubkey, - ) -> std::result::Result, solana_program_error::ProgramError> { - let data = account_info.try_borrow_data().map_err(__anchor_to_program_error)?; - let discriminator = &data[0..8]; - - match discriminator { - #(#compress_arms)* - _ => Err(__anchor_to_program_error(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch)) - } + /// Generate the compress context implementation module. + /// + /// Creates a module containing the `CompressContext` trait implementation + /// that handles discriminator-based deserialization and compression. + pub fn generate_context_impl(&self) -> Result { + let lifetime: syn::Lifetime = syn::parse_quote!('info); + + let compress_arms: Vec<_> = self.account_types.iter().map(|account_type| { + let name = qualify_type_with_crate(account_type); + quote! { + d if d == #name::LIGHT_DISCRIMINATOR => { + drop(data); + let data_borrow = account_info.try_borrow_data().map_err(__anchor_to_program_error)?; + let mut account_data = #name::try_deserialize(&mut &data_borrow[..]) + .map_err(__anchor_to_program_error)?; + drop(data_borrow); + + let compressed_info = light_sdk::compressible::compress_account::prepare_account_for_compression::<#name>( + program_id, + account_info, + &mut account_data, + meta, + cpi_accounts, + &compression_config.address_space, + )?; + Ok(Some(compressed_info)) } } - } - }) -} + }).collect(); -// ============================================================================= -// COMPRESS PROCESSOR -// ============================================================================= + Ok(syn::parse_quote! { + mod __compress_context_impl { + use super::*; + use light_sdk::LightDiscriminator; + use light_sdk::compressible::HasCompressionInfo; -pub fn generate_process_compress_accounts_idempotent() -> Result { - Ok(syn::parse_quote! { - #[inline(never)] - pub fn process_compress_accounts_idempotent<'info>( - accounts: &CompressAccountsIdempotent<'info>, - remaining_accounts: &[solana_account_info::AccountInfo<'info>], - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - light_sdk::compressible::compress_runtime::process_compress_pda_accounts_idempotent( - accounts, - remaining_accounts, - compressed_accounts, - system_accounts_offset, - LIGHT_CPI_SIGNER, - &crate::ID, - ) - .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) - } - }) -} + #[inline(always)] + fn __anchor_to_program_error>(e: E) -> solana_program_error::ProgramError { + let err: anchor_lang::error::Error = e.into(); + let program_error: anchor_lang::prelude::ProgramError = err.into(); + let code = match program_error { + anchor_lang::prelude::ProgramError::Custom(code) => code, + _ => 0, + }; + solana_program_error::ProgramError::Custom(code) + } -// ============================================================================= -// COMPRESS INSTRUCTION ENTRYPOINT -// ============================================================================= + impl<#lifetime> light_sdk::compressible::CompressContext<#lifetime> for CompressAccountsIdempotent<#lifetime> { + fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &*self.fee_payer + } -pub fn generate_compress_instruction_entrypoint() -> Result { - Ok(syn::parse_quote! { - #[inline(never)] - #[allow(clippy::too_many_arguments)] - pub fn compress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, CompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - __processor_functions::process_compress_accounts_idempotent( - &ctx.accounts, - &ctx.remaining_accounts, - compressed_accounts, - system_accounts_offset, - ) - } - }) -} + fn config(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &self.config + } -// ============================================================================= -// COMPRESS ACCOUNTS STRUCT -// ============================================================================= + fn rent_sponsor(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &self.rent_sponsor + } -pub fn generate_compress_accounts_struct(variant: InstructionVariant) -> Result { - // Only Mixed variant is supported - PdaOnly and TokenOnly are not implemented - match variant { - InstructionVariant::PdaOnly | InstructionVariant::TokenOnly => { - unreachable!("compress_accounts_struct only supports Mixed variant") - } - InstructionVariant::Mixed => {} + fn compression_authority(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &self.compression_authority + } + + fn compress_pda_account( + &self, + account_info: &solana_account_info::AccountInfo<#lifetime>, + meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'_, #lifetime>, + compression_config: &light_sdk::compressible::CompressibleConfig, + program_id: &solana_pubkey::Pubkey, + ) -> std::result::Result, solana_program_error::ProgramError> { + let data = account_info.try_borrow_data().map_err(__anchor_to_program_error)?; + let discriminator = &data[0..8]; + + match discriminator { + #(#compress_arms)* + _ => Err(__anchor_to_program_error(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch)) + } + } + } + } + }) } - Ok(syn::parse_quote! { - #[derive(Accounts)] - pub struct CompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// CHECK: Checked by SDK - pub config: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub compression_authority: AccountInfo<'info>, - } - }) -} + /// Generate the processor function for compress accounts. + pub fn generate_processor(&self) -> Result { + Ok(syn::parse_quote! { + #[inline(never)] + pub fn process_compress_accounts_idempotent<'info>( + accounts: &CompressAccountsIdempotent<'info>, + remaining_accounts: &[solana_account_info::AccountInfo<'info>], + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + light_sdk::compressible::compress_runtime::process_compress_pda_accounts_idempotent( + accounts, + remaining_accounts, + compressed_accounts, + system_accounts_offset, + LIGHT_CPI_SIGNER, + &crate::ID, + ) + .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) + } + }) + } -// ============================================================================= -// VALIDATION AND ERROR CODES -// ============================================================================= + /// Generate the compress instruction entrypoint function. + pub fn generate_entrypoint(&self) -> Result { + Ok(syn::parse_quote! { + #[inline(never)] + #[allow(clippy::too_many_arguments)] + pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, CompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + __processor_functions::process_compress_accounts_idempotent( + &ctx.accounts, + &ctx.remaining_accounts, + compressed_accounts, + system_accounts_offset, + ) + } + }) + } -#[inline(never)] -pub fn validate_compressed_account_sizes(account_types: &[Type]) -> Result { - let size_checks: Vec<_> = account_types.iter().map(|account_type| { - // Qualify with crate:: to ensure it's accessible from generated code - let qualified_type = qualify_type_with_crate(account_type); - quote! { - const _: () = { - const COMPRESSED_SIZE: usize = 8 + <#qualified_type as light_sdk::compressible::compression_info::CompressedInitSpace>::COMPRESSED_INIT_SPACE; - if COMPRESSED_SIZE > 800 { - panic!(concat!( - "Compressed account '", stringify!(#qualified_type), "' exceeds 800-byte compressible account size limit. If you need support for larger accounts, send a message to team@lightprotocol.com" - )); - } - }; - } - }).collect(); + /// Generate the compress accounts struct. + /// + /// The accounts struct is the same for all variants since it provides + /// shared infrastructure for compression operations. For `TokenOnly`, + /// the struct is still generated but PDA compression will return errors. + pub fn generate_accounts_struct(&self) -> Result { + // All variants use the same accounts struct - it's shared infrastructure + // for compression operations. The variant behavior is determined by + // the context impl, not the accounts struct. + Ok(syn::parse_quote! { + #[derive(Accounts)] + pub struct CompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// CHECK: Checked by SDK + pub config: AccountInfo<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub compression_authority: AccountInfo<'info>, + } + }) + } - Ok(quote! { #(#size_checks)* }) -} + /// Generate compile-time size validation for compressed accounts. + pub fn generate_size_validation(&self) -> Result { + let size_checks: Vec<_> = self.account_types.iter().map(|account_type| { + let qualified_type = qualify_type_with_crate(account_type); + quote! { + const _: () = { + const COMPRESSED_SIZE: usize = 8 + <#qualified_type as light_sdk::compressible::compression_info::CompressedInitSpace>::COMPRESSED_INIT_SPACE; + if COMPRESSED_SIZE > 800 { + panic!(concat!( + "Compressed account '", stringify!(#qualified_type), "' exceeds 800-byte compressible account size limit. If you need support for larger accounts, send a message to team@lightprotocol.com" + )); + } + }; + } + }).collect(); -#[inline(never)] -pub fn generate_error_codes(variant: InstructionVariant) -> Result { - // Only Mixed variant is supported - PdaOnly and TokenOnly are not implemented - match variant { - InstructionVariant::PdaOnly | InstructionVariant::TokenOnly => { - unreachable!("generate_error_codes only supports Mixed variant") - } - InstructionVariant::Mixed => {} + Ok(quote! { #(#size_checks)* }) } - Ok(quote! { - #[error_code] - pub enum RentFreeInstructionError { - #[msg("Rent sponsor mismatch")] - InvalidRentSponsor, - #[msg("Missing seed account")] - MissingSeedAccount, - #[msg("Seed value does not match account data")] - SeedMismatch, - #[msg("Not implemented")] - CTokenDecompressionNotImplemented, - #[msg("Not implemented")] - PdaDecompressionNotImplemented, - #[msg("Not implemented")] - TokenCompressionNotImplemented, - #[msg("Not implemented")] - PdaCompressionNotImplemented, - } - }) + /// Generate the error codes enum. + /// + /// The error codes enum is the same for all variants. It includes all + /// possible error conditions even if some don't apply to specific variants. + /// This ensures consistent error handling across different instruction types. + pub fn generate_error_codes(&self) -> Result { + // All variants use the same error codes - shared infrastructure + // that covers all possible error conditions. + Ok(quote! { + #[error_code] + pub enum RentFreeInstructionError { + #[msg("Rent sponsor mismatch")] + InvalidRentSponsor, + #[msg("Missing seed account")] + MissingSeedAccount, + #[msg("Seed value does not match account data")] + SeedMismatch, + #[msg("Not implemented")] + CTokenDecompressionNotImplemented, + #[msg("Not implemented")] + PdaDecompressionNotImplemented, + #[msg("Not implemented")] + TokenCompressionNotImplemented, + #[msg("Not implemented")] + PdaCompressionNotImplemented, + } + }) + } } diff --git a/sdk-libs/macros/src/rentfree/program/crate_context.rs b/sdk-libs/macros/src/rentfree/program/crate_context.rs index a46d18e237..8e59bab79d 100644 --- a/sdk-libs/macros/src/rentfree/program/crate_context.rs +++ b/sdk-libs/macros/src/rentfree/program/crate_context.rs @@ -13,6 +13,12 @@ use std::{ use syn::{Item, ItemStruct}; +// // ============================================================================= + +// ============================================================================= +// CRATE CONTEXT +// ============================================================================= + /// Context containing all parsed modules in the crate. pub struct CrateContext { modules: BTreeMap, @@ -65,12 +71,6 @@ impl CrateContext { .collect() } - /// Get a reference to a specific module by path (e.g., "crate::instruction_accounts"). - #[allow(dead_code)] - pub fn module(&self, path: &str) -> Option<&ParsedModule> { - self.modules.get(path) - } - /// Get the field names of a struct by its type. /// /// The type can be a simple identifier (e.g., "SinglePubkeyRecord") or @@ -102,15 +102,6 @@ impl CrateContext { /// A parsed module containing its items. pub struct ParsedModule { - /// Module name (e.g., "instruction_accounts") - #[allow(dead_code)] - name: String, - /// File path where this module is defined - #[allow(dead_code)] - file: PathBuf, - /// Full module path (e.g., "crate::instruction_accounts") - #[allow(dead_code)] - path: String, /// All items in the module items: Vec, } @@ -143,9 +134,6 @@ impl ParsedModule { // Create the root module let root_module = ParsedModule { - name: root_name.to_string(), - file: root.to_path_buf(), - path: module_path.to_string(), items: file.items.clone(), }; modules.insert(module_path.to_string(), root_module); @@ -159,9 +147,6 @@ impl ParsedModule { if let Some((_, items)) = &item_mod.content { // Inline module: mod foo { ... } let inline_module = ParsedModule { - name: mod_name.clone(), - file: root.to_path_buf(), - path: child_path.clone(), items: items.clone(), }; modules.insert(child_path, inline_module); diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index a2148e7fc0..be92ceb5e0 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -1,157 +1,264 @@ //! Decompress code generation. +//! +//! This module provides the `DecompressBuilder` for generating decompress instruction +//! code including context implementation, processor, entrypoint, accounts struct, +//! and PDA seed provider implementations. use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Ident, Result}; +use syn::{Ident, Result, Type}; use super::{ expr_traversal::transform_expr_for_ctx_seeds, - parsing::{InstructionVariant, SeedElement, TokenSeedSpec}, + parsing::{SeedElement, TokenSeedSpec}, seed_utils::ctx_fields_to_set, variant_enum::PdaCtxSeedInfo, }; use crate::rentfree::shared_utils::{is_constant_identifier, qualify_type_with_crate}; // ============================================================================= -// DECOMPRESS CONTEXT IMPL +// DECOMPRESS BUILDER // ============================================================================= -pub fn generate_decompress_context_impl( +/// Builder for generating decompress instruction code. +/// +/// Encapsulates all data needed to generate decompress-related code: +/// context implementation, processor function, instruction entrypoint, +/// accounts struct, and PDA seed provider implementations. +pub(super) struct DecompressBuilder { + /// PDA context seed information for each variant. pda_ctx_seeds: Vec, + /// Token variant identifier (e.g., TokenAccountVariant). token_variant_ident: Ident, -) -> Result { - let lifetime: syn::Lifetime = syn::parse_quote!('info); + /// Account types that can be decompressed. + account_types: Vec, + /// PDA seed specifications. + pda_seeds: Option>, +} - let trait_impl = - crate::rentfree::account::decompress_context::generate_decompress_context_trait_impl( +impl DecompressBuilder { + /// Create a new DecompressBuilder with all required configuration. + /// + /// # Arguments + /// * `pda_ctx_seeds` - PDA context seed information for each variant + /// * `token_variant_ident` - Token variant identifier + /// * `account_types` - Account types that can be decompressed + /// * `pda_seeds` - PDA seed specifications + pub fn new( + pda_ctx_seeds: Vec, + token_variant_ident: Ident, + account_types: Vec, + pda_seeds: Option>, + ) -> Self { + Self { pda_ctx_seeds, token_variant_ident, - lifetime, - )?; + account_types, + pda_seeds, + } + } - Ok(syn::parse_quote! { - mod __decompress_context_impl { - use super::*; + // ------------------------------------------------------------------------- + // Code Generation Methods + // ------------------------------------------------------------------------- - #trait_impl - } - }) -} + /// Generate the decompress context implementation module. + pub fn generate_context_impl(&self) -> Result { + let lifetime: syn::Lifetime = syn::parse_quote!('info); -// ============================================================================= -// DECOMPRESS PROCESSOR -// ============================================================================= + let trait_impl = + crate::rentfree::account::decompress_context::generate_decompress_context_trait_impl( + self.pda_ctx_seeds.clone(), + self.token_variant_ident.clone(), + lifetime, + )?; -pub fn generate_process_decompress_accounts_idempotent() -> Result { - Ok(syn::parse_quote! { - #[inline(never)] - pub fn process_decompress_accounts_idempotent<'info>( - accounts: &DecompressAccountsIdempotent<'info>, - remaining_accounts: &[solana_account_info::AccountInfo<'info>], - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - light_sdk::compressible::process_decompress_accounts_idempotent( - accounts, - remaining_accounts, - compressed_accounts, - proof, - system_accounts_offset, - LIGHT_CPI_SIGNER, - &crate::ID, - None, - ) - .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) - } - }) -} + Ok(syn::parse_quote! { + mod __decompress_context_impl { + use super::*; -// ============================================================================= -// DECOMPRESS INSTRUCTION ENTRYPOINT -// ============================================================================= + #trait_impl + } + }) + } -pub fn generate_decompress_instruction_entrypoint() -> Result { - Ok(syn::parse_quote! { - #[inline(never)] - pub fn decompress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - __processor_functions::process_decompress_accounts_idempotent( - &ctx.accounts, - &ctx.remaining_accounts, - proof, - compressed_accounts, - system_accounts_offset, - ) - } - }) -} + /// Generate the processor function for decompress accounts. + pub fn generate_processor(&self) -> Result { + Ok(syn::parse_quote! { + #[inline(never)] + pub fn process_decompress_accounts_idempotent<'info>( + accounts: &DecompressAccountsIdempotent<'info>, + remaining_accounts: &[solana_account_info::AccountInfo<'info>], + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + light_sdk::compressible::process_decompress_accounts_idempotent( + accounts, + remaining_accounts, + compressed_accounts, + proof, + system_accounts_offset, + LIGHT_CPI_SIGNER, + &crate::ID, + None, + ) + .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) + } + }) + } -// ============================================================================= -// DECOMPRESS ACCOUNTS STRUCT -// ============================================================================= + /// Generate the decompress instruction entrypoint function. + pub fn generate_entrypoint(&self) -> Result { + Ok(syn::parse_quote! { + #[inline(never)] + pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + __processor_functions::process_decompress_accounts_idempotent( + &ctx.accounts, + &ctx.remaining_accounts, + proof, + compressed_accounts, + system_accounts_offset, + ) + } + }) + } -#[inline(never)] -pub fn generate_decompress_accounts_struct(variant: InstructionVariant) -> Result { - // Only Mixed variant is supported - PdaOnly and TokenOnly are not implemented - match variant { - InstructionVariant::PdaOnly | InstructionVariant::TokenOnly => { - unreachable!("decompress_accounts_struct only supports Mixed variant") - } - InstructionVariant::Mixed => {} + /// Generate the decompress accounts struct. + /// + /// The accounts struct is the same for all variants since it provides + /// shared infrastructure for decompression operations. The variant behavior + /// is determined by the context implementation, not the accounts struct. + pub fn generate_accounts_struct(&self) -> Result { + Ok(syn::parse_quote! { + #[derive(Accounts)] + pub struct DecompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// CHECK: Checked by SDK + pub config: AccountInfo<'info>, + /// CHECK: anyone can pay + #[account(mut)] + pub rent_sponsor: UncheckedAccount<'info>, + /// CHECK: optional - only needed if decompressing tokens + #[account(mut)] + pub ctoken_rent_sponsor: Option>, + /// CHECK: + #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] + pub light_token_program: Option>, + /// CHECK: + #[account(address = solana_pubkey::pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] + pub ctoken_cpi_authority: Option>, + /// CHECK: Checked by SDK + pub ctoken_config: Option>, + } + }) } - let account_fields = vec![ - quote! { - #[account(mut)] - pub fee_payer: Signer<'info> - }, - quote! { - /// CHECK: Checked by SDK - pub config: AccountInfo<'info> - }, - quote! { - /// CHECK: anyone can pay - #[account(mut)] - pub rent_sponsor: UncheckedAccount<'info> - }, - quote! { - /// CHECK: optional - only needed if decompressing tokens - #[account(mut)] - pub ctoken_rent_sponsor: Option> - }, - quote! { - /// CHECK: - #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] - pub light_token_program: Option> - }, - quote! { - /// CHECK: - #[account(address = solana_pubkey::pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] - pub ctoken_cpi_authority: Option> - }, - quote! { - /// CHECK: Checked by SDK - pub ctoken_config: Option> - }, - ]; - - let struct_def = quote! { - #[derive(Accounts)] - pub struct DecompressAccountsIdempotent<'info> { - #(#account_fields,)* + /// Generate PDA seed provider implementations. + pub fn generate_seed_provider_impls(&self) -> Result> { + let pda_seed_specs = self.pda_seeds.as_ref().ok_or_else(|| { + let span_source = self + .account_types + .first() + .map(|t| quote::quote!(#t)) + .unwrap_or_else(|| quote::quote!(unknown)); + super::parsing::macro_error!(span_source, "No seed specifications provided") + })?; + + let mut results = Vec::with_capacity(self.pda_ctx_seeds.len()); + + for ctx_info in self.pda_ctx_seeds.iter() { + let variant_str = ctx_info.variant_name.to_string(); + let spec = pda_seed_specs + .iter() + .find(|s| s.variant == variant_str) + .ok_or_else(|| { + super::parsing::macro_error!( + &ctx_info.variant_name, + "No seed specification for variant '{}'", + variant_str + ) + })?; + + let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", ctx_info.variant_name); + let inner_type = qualify_type_with_crate(&ctx_info.inner_type); + let ctx_fields = &ctx_info.ctx_seed_fields; + let ctx_fields_decl: Vec<_> = ctx_fields + .iter() + .map(|field| { + quote! { pub #field: solana_pubkey::Pubkey } + }) + .collect(); + + let ctx_seeds_struct = if ctx_fields.is_empty() { + quote! { + #[derive(Default)] + pub struct #ctx_seeds_struct_name; + } + } else { + quote! { + #[derive(Default)] + pub struct #ctx_seeds_struct_name { + #(#ctx_fields_decl),* + } + } + }; + + let params_only_fields = &ctx_info.params_only_seed_fields; + let seed_derivation = generate_pda_seed_derivation_for_trait_with_ctx_seeds( + spec, + ctx_fields, + &ctx_info.state_field_names, + params_only_fields, + )?; + + let has_params_only = !params_only_fields.is_empty(); + let seed_params_impl = if has_params_only { + quote! { + #ctx_seeds_struct + + impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, SeedParams> for #inner_type { + fn derive_pda_seeds_with_accounts( + &self, + program_id: &solana_pubkey::Pubkey, + ctx_seeds: &#ctx_seeds_struct_name, + seed_params: &SeedParams, + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { + #seed_derivation + } + } + } + } else { + quote! { + #ctx_seeds_struct + + impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, SeedParams> for #inner_type { + fn derive_pda_seeds_with_accounts( + &self, + program_id: &solana_pubkey::Pubkey, + ctx_seeds: &#ctx_seeds_struct_name, + _seed_params: &SeedParams, + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { + #seed_derivation + } + } + } + }; + results.push(seed_params_impl); } - }; - syn::parse2(struct_def) + Ok(results) + } } // ============================================================================= -// PDA SEED DERIVATION +// PDA SEED DERIVATION (Internal helpers used by DecompressBuilder) // ============================================================================= /// Generate PDA seed derivation that uses CtxSeeds struct instead of DecompressAccountsIdempotent. @@ -196,9 +303,10 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( } } - // Handle uppercase constants + // Handle uppercase constants (single-segment and multi-segment paths) if let syn::Expr::Path(path_expr) = &**expr { if let Some(ident) = path_expr.path.get_ident() { + // Single-segment path like AUTH_SEED let ident_str = ident.to_string(); if is_constant_identifier(&ident_str) { seed_refs.push( @@ -206,6 +314,14 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( ); continue; } + } else if let Some(last_seg) = path_expr.path.segments.last() { + // Multi-segment path like crate::AUTH_SEED + if is_constant_identifier(&last_seg.ident.to_string()) { + let path = &path_expr.path; + seed_refs + .push(quote! { { let __seed: &[u8] = #path.as_ref(); __seed } }); + continue; + } } } @@ -281,15 +397,6 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( }) } -/// Check if a seed expression is a params-only seed (data.field where field doesn't exist on state) -#[allow(dead_code)] -fn is_params_only_seed( - expr: &syn::Expr, - state_field_names: &std::collections::HashSet, -) -> bool { - get_params_only_field_name(expr, state_field_names).is_some() -} - /// Get the field name from a params-only seed expression. /// Returns Some(field_name) if the expression is a data.field where field doesn't exist on state. fn get_params_only_field_name( @@ -319,114 +426,3 @@ fn get_params_only_field_name( _ => None, } } - -// ============================================================================= -// PDA SEED PROVIDER IMPLS -// ============================================================================= - -#[inline(never)] -pub fn generate_pda_seed_provider_impls( - account_types: &[syn::Type], - pda_ctx_seeds: &[PdaCtxSeedInfo], - pda_seeds: &Option>, -) -> Result> { - let pda_seed_specs = pda_seeds.as_ref().ok_or_else(|| { - // Use first account type for error span, or create a dummy span - let span_source = account_types - .first() - .map(|t| quote::quote!(#t)) - .unwrap_or_else(|| quote::quote!(unknown)); - super::parsing::macro_error!(span_source, "No seed specifications provided") - })?; - - let mut results = Vec::with_capacity(pda_ctx_seeds.len()); - - // Iterate over pda_ctx_seeds which has both variant_name and inner_type - for ctx_info in pda_ctx_seeds.iter() { - // Match spec by variant_name (field name based) - let variant_str = ctx_info.variant_name.to_string(); - let spec = pda_seed_specs - .iter() - .find(|s| s.variant == variant_str) - .ok_or_else(|| { - super::parsing::macro_error!( - &ctx_info.variant_name, - "No seed specification for variant '{}'", - variant_str - ) - })?; - - // Use variant_name for struct naming (e.g., RecordCtxSeeds) - let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", ctx_info.variant_name); - // Use inner_type for the impl (e.g., impl ... for crate::SinglePubkeyRecord) - // Qualify with crate:: to ensure it's accessible from generated code - let inner_type = qualify_type_with_crate(&ctx_info.inner_type); - let ctx_fields = &ctx_info.ctx_seed_fields; - let ctx_fields_decl: Vec<_> = ctx_fields - .iter() - .map(|field| { - quote! { pub #field: solana_pubkey::Pubkey } - }) - .collect(); - - let ctx_seeds_struct = if ctx_fields.is_empty() { - quote! { - #[derive(Default)] - pub struct #ctx_seeds_struct_name; - } - } else { - quote! { - #[derive(Default)] - pub struct #ctx_seeds_struct_name { - #(#ctx_fields_decl),* - } - } - }; - - let params_only_fields = &ctx_info.params_only_seed_fields; - let seed_derivation = generate_pda_seed_derivation_for_trait_with_ctx_seeds( - spec, - ctx_fields, - &ctx_info.state_field_names, - params_only_fields, - )?; - - // Generate impl for inner_type, but use variant-based struct name - // Use SeedParams if there are params-only fields, otherwise use () - let has_params_only = !params_only_fields.is_empty(); - let seed_params_impl = if has_params_only { - quote! { - #ctx_seeds_struct - - impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, SeedParams> for #inner_type { - fn derive_pda_seeds_with_accounts( - &self, - program_id: &solana_pubkey::Pubkey, - ctx_seeds: &#ctx_seeds_struct_name, - seed_params: &SeedParams, - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { - #seed_derivation - } - } - } - } else { - quote! { - #ctx_seeds_struct - - impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, SeedParams> for #inner_type { - fn derive_pda_seeds_with_accounts( - &self, - program_id: &solana_pubkey::Pubkey, - ctx_seeds: &#ctx_seeds_struct_name, - _seed_params: &SeedParams, - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { - #seed_derivation - } - } - } - }; - results.push(seed_params_impl); - } - - Ok(results) -} diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index 1ca6f24e1b..f9d1b4968a 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -10,21 +10,13 @@ pub use super::parsing::{ SeedElement, TokenSeedSpec, }; use super::{ - compress::{ - generate_compress_accounts_struct, generate_compress_context_impl, - generate_compress_instruction_entrypoint, generate_error_codes, - generate_process_compress_accounts_idempotent, validate_compressed_account_sizes, - }, - decompress::{ - generate_decompress_accounts_struct, generate_decompress_context_impl, - generate_decompress_instruction_entrypoint, generate_pda_seed_provider_impls, - generate_process_decompress_accounts_idempotent, - }, + compress::CompressBuilder, + decompress::DecompressBuilder, parsing::{ convert_classified_to_seed_elements, convert_classified_to_seed_elements_vec, extract_context_and_params, macro_error, wrap_function_with_rentfree, }, - variant_enum::PdaCtxSeedInfo, + variant_enum::{PdaCtxSeedInfo, RentFreeVariantBuilder, TokenVariantBuilder}, }; use crate::{ rentfree::shared_utils::{ident_to_type, qualify_type_with_crate}, @@ -45,9 +37,10 @@ fn codegen( instruction_data: Vec, crate_ctx: &super::crate_context::CrateContext, ) -> Result { - let size_validation_checks = validate_compressed_account_sizes(&account_types)?; - - let content = module.content.as_mut().unwrap(); + let content = match module.content.as_mut() { + Some(content) => content, + None => return Err(macro_error!(module, "Module must have a body")), + }; // Insert anchor_lang::prelude::* import at the beginning of the module // This ensures Accounts, Signer, AccountInfo, Result, error_code etc. are in scope @@ -58,7 +51,7 @@ fn codegen( content.1.insert(0, anchor_import); let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { if !token_seed_specs.is_empty() { - super::variant_enum::generate_ctoken_account_variant_enum(token_seed_specs)? + TokenVariantBuilder::new(token_seed_specs).build()? } else { crate::rentfree::account::utils::generate_empty_ctoken_enum() } @@ -112,8 +105,7 @@ fn codegen( }) .unwrap_or_default(); - let enum_and_traits = - super::variant_enum::compressed_account_variant_with_ctx_seeds(&pda_ctx_seeds)?; + let enum_and_traits = RentFreeVariantBuilder::new(&pda_ctx_seeds).build()?; // Collect all unique params-only seed fields across all variants for SeedParams struct // Use BTreeMap for deterministic ordering @@ -265,11 +257,26 @@ fn codegen( } }; - let error_codes = generate_error_codes(instruction_variant)?; - let decompress_accounts = generate_decompress_accounts_struct(instruction_variant)?; + // Create CompressBuilder to generate all compress-related code + let compress_builder = CompressBuilder::new(account_types.clone(), instruction_variant); + compress_builder.validate()?; + + let size_validation_checks = compress_builder.generate_size_validation()?; + let error_codes = compress_builder.generate_error_codes()?; - let pda_seed_provider_impls = - generate_pda_seed_provider_impls(&account_types, &pda_ctx_seeds, &pda_seeds)?; + let token_variant_name = format_ident!("TokenAccountVariant"); + + // Create DecompressBuilder to generate all decompress-related code + let decompress_builder = DecompressBuilder::new( + pda_ctx_seeds.clone(), + token_variant_name, + account_types.clone(), + pda_seeds.clone(), + ); + // Note: DecompressBuilder validation is optional for now since pda_seeds may be empty for TokenOnly + + let decompress_accounts = decompress_builder.generate_accounts_struct()?; + let pda_seed_provider_impls = decompress_builder.generate_seed_provider_impls()?; let trait_impls: syn::ItemMod = syn::parse_quote! { mod __trait_impls { @@ -283,17 +290,14 @@ fn codegen( } }; - let token_variant_name = format_ident!("TokenAccountVariant"); - - let decompress_context_impl = - generate_decompress_context_impl(pda_ctx_seeds.clone(), token_variant_name)?; - let decompress_processor_fn = generate_process_decompress_accounts_idempotent()?; - let decompress_instruction = generate_decompress_instruction_entrypoint()?; + let decompress_context_impl = decompress_builder.generate_context_impl()?; + let decompress_processor_fn = decompress_builder.generate_processor()?; + let decompress_instruction = decompress_builder.generate_entrypoint()?; - let compress_accounts = generate_compress_accounts_struct(instruction_variant)?; - let compress_context_impl = generate_compress_context_impl(account_types.clone())?; - let compress_processor_fn = generate_process_compress_accounts_idempotent()?; - let compress_instruction = generate_compress_instruction_entrypoint()?; + let compress_accounts = compress_builder.generate_accounts_struct()?; + let compress_context_impl = compress_builder.generate_context_impl()?; + let compress_processor_fn = compress_builder.generate_processor()?; + let compress_instruction = compress_builder.generate_entrypoint()?; let module_tokens = quote! { mod __processor_functions { diff --git a/sdk-libs/macros/src/rentfree/program/parsing.rs b/sdk-libs/macros/src/rentfree/program/parsing.rs index 59b73d2a06..96d9584b1b 100644 --- a/sdk-libs/macros/src/rentfree/program/parsing.rs +++ b/sdk-libs/macros/src/rentfree/program/parsing.rs @@ -193,7 +193,7 @@ fn parse_authority_seeds(content: ParseStream) -> Result> { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum SeedElement { Literal(LitStr), Expression(Box), @@ -411,9 +411,8 @@ pub fn wrap_function_with_rentfree(fn_item: &ItemFn, params_ident: &Ident) -> It // Phase 1: Pre-init (creates mints via CPI context write, registers compressed addresses) use light_sdk::compressible::{LightPreInit, LightFinalize}; let _ = ctx.accounts.light_pre_init(ctx.remaining_accounts, &#params_ident) - .map_err(|e| { - let pe: solana_program_error::ProgramError = e.into(); - pe + .map_err(|e: light_sdk::error::LightSdkError| -> solana_program_error::ProgramError { + e.into() })?; // Execute the original handler body diff --git a/sdk-libs/macros/src/rentfree/program/seed_utils.rs b/sdk-libs/macros/src/rentfree/program/seed_utils.rs index 93b875bb6b..d2675a7fab 100644 --- a/sdk-libs/macros/src/rentfree/program/seed_utils.rs +++ b/sdk-libs/macros/src/rentfree/program/seed_utils.rs @@ -53,9 +53,10 @@ pub fn seed_element_to_ref_expr(seed: &SeedElement, config: &SeedConversionConfi } } - // Handle uppercase constants + // Handle uppercase constants (single-segment and multi-segment paths) if let syn::Expr::Path(path_expr) = &**expr { if let Some(ident) = path_expr.path.get_ident() { + // Single-segment path like AUTH_SEED let ident_str = ident.to_string(); if is_constant_identifier(&ident_str) { if config.handle_light_cpi_signer && ident_str == "LIGHT_CPI_SIGNER" { @@ -64,6 +65,12 @@ pub fn seed_element_to_ref_expr(seed: &SeedElement, config: &SeedConversionConfi return quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }; } } + } else if let Some(last_seg) = path_expr.path.segments.last() { + // Multi-segment path like crate::AUTH_SEED + if is_constant_identifier(&last_seg.ident.to_string()) { + let path = &path_expr.path; + return quote! { { let __seed: &[u8] = #path.as_ref(); __seed } }; + } } } @@ -74,8 +81,8 @@ pub fn seed_element_to_ref_expr(seed: &SeedElement, config: &SeedConversionConfi } } - // Fallback - quote! { (#expr).as_ref() } + // Fallback - wrap in type-annotated block to ensure type inference succeeds + quote! { { let __seed: &[u8] = (#expr).as_ref(); __seed } } } } } diff --git a/sdk-libs/macros/src/rentfree/program/variant_enum.rs b/sdk-libs/macros/src/rentfree/program/variant_enum.rs index f986d649c1..b3411cc089 100644 --- a/sdk-libs/macros/src/rentfree/program/variant_enum.rs +++ b/sdk-libs/macros/src/rentfree/program/variant_enum.rs @@ -7,577 +7,765 @@ use crate::rentfree::shared_utils::{ make_packed_type, make_packed_variant_name, qualify_type_with_crate, }; -/// Info about ctx.* seeds for a PDA type -#[derive(Clone, Debug)] -pub struct PdaCtxSeedInfo { - /// The variant name (derived from field name, e.g., "Record" from field "record") - pub variant_name: Ident, - /// The inner type (e.g., crate::state::SinglePubkeyRecord - preserves full path) - pub inner_type: Type, - /// Field names from ctx.accounts.XXX references in seeds - pub ctx_seed_fields: Vec, - /// Field names that exist on the state struct (for filtering data.* seeds) - pub state_field_names: std::collections::HashSet, - /// Params-only seed fields (name, type, has_conversion) - seeds from params.* that don't exist on state - /// The bool indicates whether a conversion method like to_le_bytes() is applied - pub params_only_seed_fields: Vec<(Ident, Type, bool)>, +// ============================================================================= +// RENTFREE VARIANT BUILDER +// ============================================================================= + +/// Builder for generating `RentFreeAccountVariant` enum and its trait implementations. +/// +/// Encapsulates the PDA context seed info and configuration needed to generate +/// all variant-related code: enum definition, trait impls, and wrapper struct. +pub(super) struct RentFreeVariantBuilder<'a> { + /// PDA context seed info for each account type. + pda_ctx_seeds: &'a [PdaCtxSeedInfo], + /// Whether to include CToken variants in the generated enum. + include_ctoken: bool, } -impl PdaCtxSeedInfo { - pub fn with_state_fields( - variant_name: Ident, - inner_type: Type, - ctx_seed_fields: Vec, - state_field_names: std::collections::HashSet, - params_only_seed_fields: Vec<(Ident, Type, bool)>, - ) -> Self { +impl<'a> RentFreeVariantBuilder<'a> { + /// Create a new RentFreeVariantBuilder with the given PDA context seeds. + /// + /// # Arguments + /// * `pda_ctx_seeds` - PDA context seed info for each account type + /// + /// # Returns + /// A new RentFreeVariantBuilder instance + pub fn new(pda_ctx_seeds: &'a [PdaCtxSeedInfo]) -> Self { Self { - variant_name, - inner_type, - ctx_seed_fields, - state_field_names, - params_only_seed_fields, + pda_ctx_seeds, + include_ctoken: true, // Default to including CToken variants } } -} -/// Enhanced function that generates variants with ctx.* seed fields. -/// Now uses variant_name for enum variant naming and inner_type for type references. -pub fn compressed_account_variant_with_ctx_seeds( - pda_ctx_seeds: &[PdaCtxSeedInfo], -) -> Result { - if pda_ctx_seeds.is_empty() { - return Err(syn::Error::new( - proc_macro2::Span::call_site(), - "At least one account type must be specified", - )); + /// Validate the builder configuration. + /// + /// Checks that at least one account type is provided. + /// + /// # Returns + /// `Ok(())` if validation passes, or a `syn::Error` describing the issue. + pub fn validate(&self) -> Result<()> { + if self.pda_ctx_seeds.is_empty() { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + "At least one account type must be specified", + )); + } + Ok(()) } - // Phase 2: Generate struct variants with ctx.* seed fields and params-only seed fields - // Uses variant_name for enum variant naming, inner_type for data field types - let account_variants = pda_ctx_seeds.iter().map(|info| { - let variant_name = &info.variant_name; - // Qualify inner_type with crate:: to ensure it's accessible from generated code - let inner_type = qualify_type_with_crate(&info.inner_type); - let packed_variant_name = make_packed_variant_name(variant_name); - // Create packed type (also qualified with crate::) - let packed_inner_type = - make_packed_type(&info.inner_type).expect("inner_type should be a valid type path"); - let ctx_fields = &info.ctx_seed_fields; - let params_only_fields = &info.params_only_seed_fields; - - // Unpacked variant: Pubkey fields for ctx.* seeds + params-only seed values - // Note: Use bare Pubkey which is in scope via `use anchor_lang::prelude::*` - let unpacked_ctx_fields = ctx_fields.iter().map(|field| { - quote! { #field: Pubkey } - }); - let unpacked_params_fields = params_only_fields.iter().map(|(field, ty, _)| { - quote! { #field: #ty } - }); + // ------------------------------------------------------------------------- + // Code Generation Methods + // ------------------------------------------------------------------------- + + /// Generate the complete enum and all trait implementations. + /// + /// This is the main entry point that combines all generated code pieces. + pub fn build(&self) -> Result { + self.validate()?; + + let enum_def = self.generate_enum_def()?; + let default_impl = self.generate_default_impl(); + let data_hasher_impl = self.generate_data_hasher_impl(); + let light_discriminator_impl = self.generate_light_discriminator_impl(); + let has_compression_info_impl = self.generate_has_compression_info_impl(); + let size_impl = self.generate_size_impl(); + let pack_impl = self.generate_pack_impl(); + let unpack_impl = self.generate_unpack_impl()?; + let rentfree_account_data_struct = self.generate_rentfree_account_data_struct(); + + Ok(quote! { + #enum_def + #default_impl + #data_hasher_impl + #light_discriminator_impl + #has_compression_info_impl + #size_impl + #pack_impl + #unpack_impl + #rentfree_account_data_struct + }) + } + + /// Generate the enum definition with all variants. + fn generate_enum_def(&self) -> Result { + let mut account_variants_tokens = Vec::new(); + for info in self.pda_ctx_seeds.iter() { + let variant_name = &info.variant_name; + let inner_type = qualify_type_with_crate(&info.inner_type); + let packed_variant_name = make_packed_variant_name(variant_name); + let packed_inner_type = make_packed_type(&info.inner_type).ok_or_else(|| { + syn::Error::new_spanned(&info.inner_type, "invalid type path for packed type") + })?; + let ctx_fields = &info.ctx_seed_fields; + let params_only_fields = &info.params_only_seed_fields; + + let unpacked_ctx_fields = ctx_fields.iter().map(|field| { + quote! { #field: Pubkey } + }); + let unpacked_params_fields = params_only_fields.iter().map(|(field, ty, _)| { + quote! { #field: #ty } + }); + + let packed_ctx_fields = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { #idx_field: u8 } + }); + let packed_params_fields = params_only_fields.iter().map(|(field, ty, _)| { + quote! { #field: #ty } + }); + + account_variants_tokens.push(quote! { + #variant_name { data: #inner_type, #(#unpacked_ctx_fields,)* #(#unpacked_params_fields,)* }, + #packed_variant_name { data: #packed_inner_type, #(#packed_ctx_fields,)* #(#packed_params_fields,)* }, + }); + } - // Packed variant: u8 index fields for ctx.* seeds + params-only seed values (same type) - let packed_ctx_fields = ctx_fields.iter().map(|field| { - let idx_field = format_ident!("{}_idx", field); - quote! { #idx_field: u8 } + let ctoken_variants = if self.include_ctoken { + quote! { + PackedCTokenData(light_token_sdk::compat::PackedCTokenData), + CTokenData(light_token_sdk::compat::CTokenData), + } + } else { + quote! {} + }; + + Ok(quote! { + #[derive(Clone, Debug, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + pub enum RentFreeAccountVariant { + #(#account_variants_tokens)* + #ctoken_variants + } + }) + } + + /// Generate the Default implementation. + fn generate_default_impl(&self) -> TokenStream { + let first = &self.pda_ctx_seeds[0]; + let first_variant = &first.variant_name; + let first_type = qualify_type_with_crate(&first.inner_type); + let first_ctx_fields = &first.ctx_seed_fields; + let first_params_only_fields = &first.params_only_seed_fields; + + let first_default_ctx_fields = first_ctx_fields.iter().map(|field| { + quote! { #field: Pubkey::default() } }); - // Params-only fields keep the same type in packed variant (not indices) - let packed_params_fields = params_only_fields.iter().map(|(field, ty, _)| { - quote! { #field: #ty } + let first_default_params_fields = first_params_only_fields.iter().map(|(field, ty, _)| { + quote! { #field: <#ty as Default>::default() } }); quote! { - #variant_name { data: #inner_type, #(#unpacked_ctx_fields,)* #(#unpacked_params_fields,)* }, - #packed_variant_name { data: #packed_inner_type, #(#packed_ctx_fields,)* #(#packed_params_fields,)* }, - } - }); - - // Phase 8: PackedCTokenData uses PackedTokenAccountVariant (with idx fields) - // CTokenData uses TokenAccountVariant (with Pubkey fields) - let enum_def = quote! { - #[derive(Clone, Debug, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] - pub enum RentFreeAccountVariant { - #(#account_variants)* - PackedCTokenData(light_token_sdk::compat::PackedCTokenData), - CTokenData(light_token_sdk::compat::CTokenData), - } - }; - - let first = &pda_ctx_seeds[0]; - let first_variant = &first.variant_name; - let first_type = qualify_type_with_crate(&first.inner_type); - let first_ctx_fields = &first.ctx_seed_fields; - let first_params_only_fields = &first.params_only_seed_fields; - let first_default_ctx_fields = first_ctx_fields.iter().map(|field| { - quote! { #field: Pubkey::default() } - }); - let first_default_params_fields = first_params_only_fields.iter().map(|(field, ty, _)| { - quote! { #field: <#ty as Default>::default() } - }); - let default_impl = quote! { - impl Default for RentFreeAccountVariant { - fn default() -> Self { - Self::#first_variant { data: #first_type::default(), #(#first_default_ctx_fields,)* #(#first_default_params_fields,)* } + impl Default for RentFreeAccountVariant { + fn default() -> Self { + Self::#first_variant { data: #first_type::default(), #(#first_default_ctx_fields,)* #(#first_default_params_fields,)* } + } } } - }; + } - let hash_match_arms = pda_ctx_seeds.iter().map(|info| { - let variant_name = &info.variant_name; - let inner_type = qualify_type_with_crate(&info.inner_type); - let packed_variant_name = format_ident!("Packed{}", variant_name); - quote! { - RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_hasher::DataHasher>::hash::(data), - RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), - } - }); - - let data_hasher_impl = quote! { - impl light_hasher::DataHasher for RentFreeAccountVariant { - fn hash(&self) -> std::result::Result<[u8; 32], light_hasher::HasherError> { - match self { - #(#hash_match_arms)* - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - } + /// Generate the DataHasher implementation. + fn generate_data_hasher_impl(&self) -> TokenStream { + let hash_match_arms = self.pda_ctx_seeds.iter().map(|info| { + let variant_name = &info.variant_name; + let inner_type = qualify_type_with_crate(&info.inner_type); + let packed_variant_name = format_ident!("Packed{}", variant_name); + quote! { + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_hasher::DataHasher>::hash::(data), + RentFreeAccountVariant::#packed_variant_name { .. } => Err(light_hasher::HasherError::EmptyInput), } - } - }; + }); - let light_discriminator_impl = quote! { - impl light_sdk::LightDiscriminator for RentFreeAccountVariant { - const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; - const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; - } - }; + let ctoken_arms = if self.include_ctoken { + quote! { + Self::PackedCTokenData(_) => Err(light_hasher::HasherError::EmptyInput), + Self::CTokenData(_) => Err(light_hasher::HasherError::EmptyInput), + } + } else { + quote! {} + }; - let compression_info_match_arms = pda_ctx_seeds.iter().map(|info| { - let variant_name = &info.variant_name; - let inner_type = qualify_type_with_crate(&info.inner_type); - let packed_variant_name = format_ident!("Packed{}", variant_name); quote! { - RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::compression_info(data), - RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), + impl light_hasher::DataHasher for RentFreeAccountVariant { + fn hash(&self) -> std::result::Result<[u8; 32], light_hasher::HasherError> { + match self { + #(#hash_match_arms)* + #ctoken_arms + } + } + } } - }); + } - let compression_info_mut_match_arms = pda_ctx_seeds.iter().map(|info| { - let variant_name = &info.variant_name; - let inner_type = qualify_type_with_crate(&info.inner_type); - let packed_variant_name = format_ident!("Packed{}", variant_name); + /// Generate the LightDiscriminator implementation. + fn generate_light_discriminator_impl(&self) -> TokenStream { quote! { - RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::compression_info_mut(data), - RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), + impl light_sdk::LightDiscriminator for RentFreeAccountVariant { + const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; + } } - }); + } - let compression_info_mut_opt_match_arms = pda_ctx_seeds.iter().map(|info| { - let variant_name = &info.variant_name; - let inner_type = qualify_type_with_crate(&info.inner_type); - let packed_variant_name = format_ident!("Packed{}", variant_name); - quote! { - RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::compression_info_mut_opt(data), - RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), - } - }); + /// Generate the HasCompressionInfo implementation. + fn generate_has_compression_info_impl(&self) -> TokenStream { + let compression_info_match_arms = self.pda_ctx_seeds.iter().map(|info| { + let variant_name = &info.variant_name; + let inner_type = qualify_type_with_crate(&info.inner_type); + let packed_variant_name = format_ident!("Packed{}", variant_name); + quote! { + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::compression_info(data), + RentFreeAccountVariant::#packed_variant_name { .. } => Err(light_sdk::error::LightSdkError::PackedVariantCompressionInfo.into()), + } + }); - let set_compression_info_none_match_arms = pda_ctx_seeds.iter().map(|info| { - let variant_name = &info.variant_name; - let inner_type = qualify_type_with_crate(&info.inner_type); - let packed_variant_name = format_ident!("Packed{}", variant_name); - quote! { - RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::set_compression_info_none(data), - RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), - } - }); - - let has_compression_info_impl = quote! { - impl light_sdk::compressible::HasCompressionInfo for RentFreeAccountVariant { - fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { - match self { - #(#compression_info_match_arms)* - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - } + let compression_info_mut_match_arms = self.pda_ctx_seeds.iter().map(|info| { + let variant_name = &info.variant_name; + let inner_type = qualify_type_with_crate(&info.inner_type); + let packed_variant_name = format_ident!("Packed{}", variant_name); + quote! { + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::compression_info_mut(data), + RentFreeAccountVariant::#packed_variant_name { .. } => Err(light_sdk::error::LightSdkError::PackedVariantCompressionInfo.into()), + } + }); + + let compression_info_mut_opt_match_arms = self.pda_ctx_seeds.iter().map(|info| { + let variant_name = &info.variant_name; + let inner_type = qualify_type_with_crate(&info.inner_type); + let packed_variant_name = format_ident!("Packed{}", variant_name); + quote! { + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::compression_info_mut_opt(data), + RentFreeAccountVariant::#packed_variant_name { .. } => panic!("compression_info_mut_opt not supported on packed variants"), } + }); - fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { - match self { - #(#compression_info_mut_match_arms)* - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - } + let set_compression_info_none_match_arms = self.pda_ctx_seeds.iter().map(|info| { + let variant_name = &info.variant_name; + let inner_type = qualify_type_with_crate(&info.inner_type); + let packed_variant_name = format_ident!("Packed{}", variant_name); + quote! { + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::set_compression_info_none(data), + RentFreeAccountVariant::#packed_variant_name { .. } => Err(light_sdk::error::LightSdkError::PackedVariantCompressionInfo.into()), } + }); - fn compression_info_mut_opt(&mut self) -> &mut Option { - match self { - #(#compression_info_mut_opt_match_arms)* - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - } + let ctoken_arms = if self.include_ctoken { + quote! { + Self::PackedCTokenData(_) | Self::CTokenData(_) => Err(light_sdk::error::LightSdkError::CTokenCompressionInfo.into()), } + } else { + quote! {} + }; - fn set_compression_info_none(&mut self) { - match self { - #(#set_compression_info_none_match_arms)* - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - } + let ctoken_arms_mut_opt = if self.include_ctoken { + quote! { + Self::PackedCTokenData(_) | Self::CTokenData(_) => panic!("compression_info_mut_opt not supported on CToken variants"), } - } - }; + } else { + quote! {} + }; - let size_match_arms = pda_ctx_seeds.iter().map(|info| { - let variant_name = &info.variant_name; - let inner_type = qualify_type_with_crate(&info.inner_type); - let packed_variant_name = format_ident!("Packed{}", variant_name); quote! { - RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::account::Size>::size(data), - RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), - } - }); - - let size_impl = quote! { - impl light_sdk::account::Size for RentFreeAccountVariant { - fn size(&self) -> usize { - match self { - #(#size_match_arms)* - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), + impl light_sdk::compressible::HasCompressionInfo for RentFreeAccountVariant { + fn compression_info(&self) -> std::result::Result<&light_sdk::compressible::CompressionInfo, solana_program_error::ProgramError> { + match self { + #(#compression_info_match_arms)* + #ctoken_arms + } + } + + fn compression_info_mut(&mut self) -> std::result::Result<&mut light_sdk::compressible::CompressionInfo, solana_program_error::ProgramError> { + match self { + #(#compression_info_mut_match_arms)* + #ctoken_arms + } + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + match self { + #(#compression_info_mut_opt_match_arms)* + #ctoken_arms_mut_opt + } + } + + fn set_compression_info_none(&mut self) -> std::result::Result<(), solana_program_error::ProgramError> { + match self { + #(#set_compression_info_none_match_arms)* + #ctoken_arms + } } } } - }; - - // Phase 2: Pack/Unpack with ctx seed fields and params-only seed fields - let pack_match_arms: Vec<_> = pda_ctx_seeds.iter().map(|info| { - let variant_name = &info.variant_name; - let inner_type = qualify_type_with_crate(&info.inner_type); - let packed_variant_name = format_ident!("Packed{}", variant_name); - let ctx_fields = &info.ctx_seed_fields; - let params_only_fields = &info.params_only_seed_fields; - - // Collect ctx field names and their idx equivalents - let ctx_field_names: Vec<_> = ctx_fields.iter().collect(); - let idx_field_names: Vec<_> = ctx_fields.iter().map(|f| format_ident!("{}_idx", f)).collect(); - let pack_ctx_seeds: Vec<_> = ctx_fields.iter().map(|field| { - let idx_field = format_ident!("{}_idx", field); - // Dereference because we're matching on &self, so field is &Pubkey - quote! { let #idx_field = remaining_accounts.insert_or_get(*#field); } - }).collect(); - - // Collect params-only field names (these are copied directly, not indexed) - let params_field_names: Vec<_> = params_only_fields.iter().map(|(f, _, _)| f).collect(); + } - // If no ctx seeds and no params-only fields - simple pack - if ctx_fields.is_empty() && params_only_fields.is_empty() { + /// Generate the Size implementation. + fn generate_size_impl(&self) -> TokenStream { + let size_match_arms = self.pda_ctx_seeds.iter().map(|info| { + let variant_name = &info.variant_name; + let inner_type = qualify_type_with_crate(&info.inner_type); + let packed_variant_name = format_ident!("Packed{}", variant_name); quote! { - RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), - RentFreeAccountVariant::#variant_name { data, .. } => RentFreeAccountVariant::#packed_variant_name { - data: <#inner_type as light_sdk::compressible::Pack>::pack(data, remaining_accounts), - }, + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::account::Size>::size(data), + RentFreeAccountVariant::#packed_variant_name { .. } => Err(solana_program_error::ProgramError::InvalidAccountData), } - } else { - // Has ctx seeds and/or params-only fields - pack data, ctx seed pubkeys, and copy params-only values + }); + + let ctoken_arms = if self.include_ctoken { quote! { - RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), - RentFreeAccountVariant::#variant_name { data, #(#ctx_field_names,)* #(#params_field_names,)* .. } => { - #(#pack_ctx_seeds)* - RentFreeAccountVariant::#packed_variant_name { - data: <#inner_type as light_sdk::compressible::Pack>::pack(data, remaining_accounts), - #(#idx_field_names,)* - #(#params_field_names: *#params_field_names,)* - } - }, + Self::PackedCTokenData(_) => Err(solana_program_error::ProgramError::InvalidAccountData), + Self::CTokenData(_) => Err(solana_program_error::ProgramError::InvalidAccountData), } - } - }).collect(); - - let pack_impl = quote! { - impl light_sdk::compressible::Pack for RentFreeAccountVariant { - type Packed = Self; - - fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { - match self { - #(#pack_match_arms)* - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(data) => { - // Use ctoken-sdk's Pack trait for CTokenData - Self::PackedCTokenData(light_token_sdk::pack::Pack::pack(data, remaining_accounts)) + } else { + quote! {} + }; + + quote! { + impl light_sdk::account::Size for RentFreeAccountVariant { + fn size(&self) -> std::result::Result { + match self { + #(#size_match_arms)* + #ctoken_arms } } } } - }; - - let unpack_match_arms: Vec<_> = pda_ctx_seeds.iter().map(|info| { - let variant_name = &info.variant_name; - let inner_type = &info.inner_type; - let packed_variant_name = make_packed_variant_name(variant_name); - // Create packed type preserving full path (e.g., crate::module::PackedMyRecord) - let packed_inner_type = make_packed_type(inner_type) - .expect("inner_type should be a valid type path"); - let ctx_fields = &info.ctx_seed_fields; - let params_only_fields = &info.params_only_seed_fields; - - // Collect ctx field names and their idx equivalents - let idx_field_names: Vec<_> = ctx_fields.iter().map(|f| format_ident!("{}_idx", f)).collect(); - let ctx_field_names: Vec<_> = ctx_fields.iter().collect(); - let unpack_ctx_seeds: Vec<_> = ctx_fields.iter().map(|field| { - let idx_field = format_ident!("{}_idx", field); - quote! { - let #field = *remaining_accounts - .get(*#idx_field as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? - .key; + } + + /// Generate the Pack implementation. + fn generate_pack_impl(&self) -> TokenStream { + let pack_match_arms: Vec<_> = self.pda_ctx_seeds.iter().map(|info| { + let variant_name = &info.variant_name; + let inner_type = qualify_type_with_crate(&info.inner_type); + let packed_variant_name = format_ident!("Packed{}", variant_name); + let ctx_fields = &info.ctx_seed_fields; + let params_only_fields = &info.params_only_seed_fields; + + let ctx_field_names: Vec<_> = ctx_fields.iter().collect(); + let idx_field_names: Vec<_> = ctx_fields.iter().map(|f| format_ident!("{}_idx", f)).collect(); + let pack_ctx_seeds: Vec<_> = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { let #idx_field = remaining_accounts.insert_or_get(*#field); } + }).collect(); + + let params_field_names: Vec<_> = params_only_fields.iter().map(|(f, _, _)| f).collect(); + + if ctx_fields.is_empty() && params_only_fields.is_empty() { + quote! { + RentFreeAccountVariant::#packed_variant_name { .. } => Err(solana_program_error::ProgramError::InvalidAccountData), + RentFreeAccountVariant::#variant_name { data, .. } => Ok(RentFreeAccountVariant::#packed_variant_name { + data: <#inner_type as light_sdk::compressible::Pack>::pack(data, remaining_accounts)?, + }), + } + } else { + quote! { + RentFreeAccountVariant::#packed_variant_name { .. } => Err(solana_program_error::ProgramError::InvalidAccountData), + RentFreeAccountVariant::#variant_name { data, #(#ctx_field_names,)* #(#params_field_names,)* .. } => { + #(#pack_ctx_seeds)* + Ok(RentFreeAccountVariant::#packed_variant_name { + data: <#inner_type as light_sdk::compressible::Pack>::pack(data, remaining_accounts)?, + #(#idx_field_names,)* + #(#params_field_names: *#params_field_names,)* + }) + }, + } } }).collect(); - // Collect params-only field names (these are copied directly, not resolved from indices) - let params_field_names: Vec<_> = params_only_fields.iter().map(|(f, _, _)| f).collect(); - - // If no ctx seeds and no params-only fields - simple unpack - if ctx_fields.is_empty() && params_only_fields.is_empty() { + let ctoken_arms = if self.include_ctoken { quote! { - RentFreeAccountVariant::#packed_variant_name { data, .. } => Ok(RentFreeAccountVariant::#variant_name { - data: <#packed_inner_type as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, - }), - RentFreeAccountVariant::#variant_name { .. } => unreachable!(), + Self::PackedCTokenData(_) => Err(solana_program_error::ProgramError::InvalidAccountData), + Self::CTokenData(data) => { + Ok(Self::PackedCTokenData(light_token_sdk::pack::Pack::pack(data, remaining_accounts)?)) + } } } else { - // Has ctx seeds and/or params-only fields - unpack data, resolve ctx seed pubkeys, and copy params-only values - quote! { - RentFreeAccountVariant::#packed_variant_name { data, #(#idx_field_names,)* #(#params_field_names,)* .. } => { - #(#unpack_ctx_seeds)* - Ok(RentFreeAccountVariant::#variant_name { + quote! {} + }; + + quote! { + impl light_sdk::compressible::Pack for RentFreeAccountVariant { + type Packed = Self; + + fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> std::result::Result { + match self { + #(#pack_match_arms)* + #ctoken_arms + } + } + } + } + } + + /// Generate the Unpack implementation. + fn generate_unpack_impl(&self) -> Result { + let mut unpack_match_arms = Vec::new(); + for info in self.pda_ctx_seeds.iter() { + let variant_name = &info.variant_name; + let inner_type = &info.inner_type; + let packed_variant_name = make_packed_variant_name(variant_name); + let packed_inner_type = make_packed_type(inner_type).ok_or_else(|| { + syn::Error::new_spanned(inner_type, "invalid type path for packed type") + })?; + let ctx_fields = &info.ctx_seed_fields; + let params_only_fields = &info.params_only_seed_fields; + + let idx_field_names: Vec<_> = ctx_fields + .iter() + .map(|f| format_ident!("{}_idx", f)) + .collect(); + let ctx_field_names: Vec<_> = ctx_fields.iter().collect(); + let unpack_ctx_seeds: Vec<_> = ctx_fields + .iter() + .map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { + let #field = *remaining_accounts + .get(*#idx_field as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key; + } + }) + .collect(); + + let params_field_names: Vec<_> = params_only_fields.iter().map(|(f, _, _)| f).collect(); + + if ctx_fields.is_empty() && params_only_fields.is_empty() { + unpack_match_arms.push(quote! { + RentFreeAccountVariant::#packed_variant_name { data, .. } => Ok(RentFreeAccountVariant::#variant_name { data: <#packed_inner_type as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, - #(#ctx_field_names,)* - #(#params_field_names: *#params_field_names,)* - }) - }, - RentFreeAccountVariant::#variant_name { .. } => unreachable!(), + }), + RentFreeAccountVariant::#variant_name { .. } => Err(solana_program_error::ProgramError::InvalidAccountData), + }); + } else { + unpack_match_arms.push(quote! { + RentFreeAccountVariant::#packed_variant_name { data, #(#idx_field_names,)* #(#params_field_names,)* .. } => { + #(#unpack_ctx_seeds)* + Ok(RentFreeAccountVariant::#variant_name { + data: <#packed_inner_type as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, + #(#ctx_field_names,)* + #(#params_field_names: *#params_field_names,)* + }) + }, + RentFreeAccountVariant::#variant_name { .. } => Err(solana_program_error::ProgramError::InvalidAccountData), + }); } } - }).collect(); - - let unpack_impl = quote! { - impl light_sdk::compressible::Unpack for RentFreeAccountVariant { - type Unpacked = Self; - - fn unpack( - &self, - remaining_accounts: &[anchor_lang::prelude::AccountInfo], - ) -> std::result::Result { - match self { - #(#unpack_match_arms)* - Self::PackedCTokenData(_) => { - // PackedCTokenData is handled separately in collect_pda_and_token - unreachable!("PackedCTokenData should not be unpacked through Unpack trait") + + let ctoken_arms = if self.include_ctoken { + quote! { + Self::PackedCTokenData(_) => Err(solana_program_error::ProgramError::InvalidAccountData), + Self::CTokenData(_data) => Err(solana_program_error::ProgramError::InvalidAccountData), + } + } else { + quote! {} + }; + + Ok(quote! { + impl light_sdk::compressible::Unpack for RentFreeAccountVariant { + type Unpacked = Self; + + fn unpack( + &self, + remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + match self { + #(#unpack_match_arms)* + #ctoken_arms } - Self::CTokenData(_data) => unreachable!(), } } + }) + } + + /// Generate the RentFreeAccountData struct. + fn generate_rentfree_account_data_struct(&self) -> TokenStream { + quote! { + #[derive(Clone, Debug, anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)] + pub struct RentFreeAccountData { + pub meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + pub data: RentFreeAccountVariant, + } } - }; + } +} - let rentfree_account_data_struct = quote! { - #[derive(Clone, Debug, anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)] - pub struct RentFreeAccountData { - pub meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - pub data: RentFreeAccountVariant, +/// Info about ctx.* seeds for a PDA type +#[derive(Clone, Debug)] +pub struct PdaCtxSeedInfo { + /// The variant name (derived from field name, e.g., "Record" from field "record") + pub variant_name: Ident, + /// The inner type (e.g., crate::state::SinglePubkeyRecord - preserves full path) + pub inner_type: Type, + /// Field names from ctx.accounts.XXX references in seeds + pub ctx_seed_fields: Vec, + /// Field names that exist on the state struct (for filtering data.* seeds) + pub state_field_names: std::collections::HashSet, + /// Params-only seed fields (name, type, has_conversion) - seeds from params.* that don't exist on state + /// The bool indicates whether a conversion method like to_le_bytes() is applied + pub params_only_seed_fields: Vec<(Ident, Type, bool)>, +} + +impl PdaCtxSeedInfo { + pub fn with_state_fields( + variant_name: Ident, + inner_type: Type, + ctx_seed_fields: Vec, + state_field_names: std::collections::HashSet, + params_only_seed_fields: Vec<(Ident, Type, bool)>, + ) -> Self { + Self { + variant_name, + inner_type, + ctx_seed_fields, + state_field_names, + params_only_seed_fields, } - }; - - let expanded = quote! { - #enum_def - #default_impl - #data_hasher_impl - #light_discriminator_impl - #has_compression_info_impl - #size_impl - #pack_impl - #unpack_impl - #rentfree_account_data_struct - }; - - Ok(expanded) + } } // ============================================================================= -// TOKEN ACCOUNT VARIANT +// TOKEN VARIANT BUILDER // ============================================================================= -/// Extract ctx.* field names from seed elements (both token seeds and authority seeds). +/// Builder for generating `TokenAccountVariant` and `PackedTokenAccountVariant` enums. /// -/// Uses the visitor-based FieldExtractor for clean AST traversal. -pub fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { - const EXCLUDED: &[&str] = &[ - "fee_payer", - "rent_sponsor", - "config", - "compression_authority", - ]; +/// Encapsulates the token seed specifications needed to generate +/// all token variant-related code: enum definitions, Pack/Unpack impls, and IntoCTokenVariant. +pub(super) struct TokenVariantBuilder<'a> { + /// Token seed specifications for each token variant. + token_seeds: &'a [TokenSeedSpec], +} - let mut all_fields = Vec::new(); - let mut seen = std::collections::HashSet::new(); +impl<'a> TokenVariantBuilder<'a> { + /// Create a new TokenVariantBuilder with the given token seeds. + /// + /// # Arguments + /// * `token_seeds` - Token seed specifications for each variant + /// + /// # Returns + /// A new TokenVariantBuilder instance + pub fn new(token_seeds: &'a [TokenSeedSpec]) -> Self { + Self { token_seeds } + } - for seed in spec.seeds.iter().chain(spec.authority.iter().flatten()) { - if let SeedElement::Expression(expr) = seed { - // Extract fields from this expression using the visitor - let fields = super::visitors::FieldExtractor::ctx_fields(EXCLUDED).extract(expr); - // Deduplicate across seeds - for field in fields { - let name = field.to_string(); - if seen.insert(name) { - all_fields.push(field); - } - } - } + // ------------------------------------------------------------------------- + // Code Generation Methods + // ------------------------------------------------------------------------- + + /// Generate the complete token variant code. + /// + /// This is the main entry point that combines all generated code pieces. + pub fn build(&self) -> Result { + let unpacked_enum = self.generate_unpacked_enum(); + let packed_enum = self.generate_packed_enum(); + let pack_impl = self.generate_pack_impl(); + let unpack_impl = self.generate_unpack_impl(); + let into_ctoken_variant_impl = self.generate_into_ctoken_variant_impl(); + + Ok(quote! { + #unpacked_enum + #packed_enum + #pack_impl + #unpack_impl + #into_ctoken_variant_impl + }) } - all_fields -} + /// Generate the unpacked TokenAccountVariant enum. + fn generate_unpacked_enum(&self) -> TokenStream { + let variants = self.token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); -/// Generate TokenAccountVariant and PackedTokenAccountVariant enums with Pack/Unpack impls. -pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Result { - let unpacked_variants = token_seeds.iter().map(|spec| { - let variant_name = &spec.variant; - let ctx_fields = extract_ctx_fields_from_token_spec(spec); + let fields = ctx_fields.iter().map(|field| { + quote! { #field: Pubkey } + }); - let fields = ctx_fields.iter().map(|field| { - quote! { #field: Pubkey } + if ctx_fields.is_empty() { + quote! { #variant_name, } + } else { + quote! { #variant_name { #(#fields,)* }, } + } }); - if ctx_fields.is_empty() { - quote! { #variant_name, } - } else { - quote! { #variant_name { #(#fields,)* }, } + quote! { + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] + pub enum TokenAccountVariant { + #(#variants)* + } } - }); - - let packed_variants = token_seeds.iter().map(|spec| { - let variant_name = &spec.variant; - let ctx_fields = extract_ctx_fields_from_token_spec(spec); + } - let fields = ctx_fields.iter().map(|field| { - let idx_field = format_ident!("{}_idx", field); - quote! { #idx_field: u8 } + /// Generate the packed PackedTokenAccountVariant enum. + fn generate_packed_enum(&self) -> TokenStream { + let variants = self.token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + let fields = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { #idx_field: u8 } + }); + + if ctx_fields.is_empty() { + quote! { #variant_name, } + } else { + quote! { #variant_name { #(#fields,)* }, } + } }); - if ctx_fields.is_empty() { - quote! { #variant_name, } - } else { - quote! { #variant_name { #(#fields,)* }, } + quote! { + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] + pub enum PackedTokenAccountVariant { + #(#variants)* + } } - }); + } - let pack_arms = token_seeds.iter().map(|spec| { - let variant_name = &spec.variant; - let ctx_fields = extract_ctx_fields_from_token_spec(spec); + /// Generate the Pack implementation for TokenAccountVariant. + fn generate_pack_impl(&self) -> TokenStream { + let arms = self.token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); - if ctx_fields.is_empty() { - quote! { - TokenAccountVariant::#variant_name => PackedTokenAccountVariant::#variant_name, - } - } else { - let field_bindings: Vec<_> = ctx_fields.iter().collect(); - let idx_fields: Vec<_> = ctx_fields - .iter() - .map(|f| format_ident!("{}_idx", f)) - .collect(); - let pack_stmts: Vec<_> = ctx_fields - .iter() - .zip(idx_fields.iter()) - .map(|(field, idx)| { - quote! { let #idx = remaining_accounts.insert_or_get(*#field); } - }) - .collect(); + if ctx_fields.is_empty() { + quote! { + TokenAccountVariant::#variant_name => Ok(PackedTokenAccountVariant::#variant_name), + } + } else { + let field_bindings: Vec<_> = ctx_fields.iter().collect(); + let idx_fields: Vec<_> = ctx_fields + .iter() + .map(|f| format_ident!("{}_idx", f)) + .collect(); + let pack_stmts: Vec<_> = ctx_fields + .iter() + .zip(idx_fields.iter()) + .map(|(field, idx)| { + quote! { let #idx = remaining_accounts.insert_or_get(*#field); } + }) + .collect(); - quote! { - TokenAccountVariant::#variant_name { #(#field_bindings,)* } => { - #(#pack_stmts)* - PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } + quote! { + TokenAccountVariant::#variant_name { #(#field_bindings,)* } => { + #(#pack_stmts)* + Ok(PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* }) + } } } - } - }); + }); - let unpack_arms = token_seeds.iter().map(|spec| { - let variant_name = &spec.variant; - let ctx_fields = extract_ctx_fields_from_token_spec(spec); + quote! { + impl light_token_sdk::pack::Pack for TokenAccountVariant { + type Packed = PackedTokenAccountVariant; - if ctx_fields.is_empty() { - quote! { - PackedTokenAccountVariant::#variant_name => Ok(TokenAccountVariant::#variant_name), - } - } else { - let idx_fields: Vec<_> = ctx_fields - .iter() - .map(|f| format_ident!("{}_idx", f)) - .collect(); - let unpack_stmts: Vec<_> = ctx_fields - .iter() - .zip(idx_fields.iter()) - .map(|(field, idx)| { - quote! { - let #field = *remaining_accounts - .get(*#idx as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? - .key; + fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> std::result::Result { + match self { + #(#arms)* } - }) - .collect(); - let field_names: Vec<_> = ctx_fields.iter().collect(); - - quote! { - PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } => { - #(#unpack_stmts)* - Ok(TokenAccountVariant::#variant_name { #(#field_names,)* }) } } } - }); + } - Ok(quote! { - #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] - pub enum TokenAccountVariant { - #(#unpacked_variants)* - } + /// Generate the Unpack implementation for PackedTokenAccountVariant. + fn generate_unpack_impl(&self) -> TokenStream { + let arms = self.token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); - #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] - pub enum PackedTokenAccountVariant { - #(#packed_variants)* - } + if ctx_fields.is_empty() { + quote! { + PackedTokenAccountVariant::#variant_name => Ok(TokenAccountVariant::#variant_name), + } + } else { + let idx_fields: Vec<_> = ctx_fields + .iter() + .map(|f| format_ident!("{}_idx", f)) + .collect(); + let unpack_stmts: Vec<_> = ctx_fields + .iter() + .zip(idx_fields.iter()) + .map(|(field, idx)| { + quote! { + let #field = *remaining_accounts + .get(*#idx as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key; + } + }) + .collect(); + let field_names: Vec<_> = ctx_fields.iter().collect(); - impl light_token_sdk::pack::Pack for TokenAccountVariant { - type Packed = PackedTokenAccountVariant; + quote! { + PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } => { + #(#unpack_stmts)* + Ok(TokenAccountVariant::#variant_name { #(#field_names,)* }) + } + } + } + }); - fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { - match self { - #(#pack_arms)* + quote! { + impl light_token_sdk::pack::Unpack for PackedTokenAccountVariant { + type Unpacked = TokenAccountVariant; + + fn unpack( + &self, + remaining_accounts: &[solana_account_info::AccountInfo], + ) -> std::result::Result { + match self { + #(#arms)* + } } } } + } - impl light_token_sdk::pack::Unpack for PackedTokenAccountVariant { - type Unpacked = TokenAccountVariant; - - fn unpack( - &self, - remaining_accounts: &[solana_account_info::AccountInfo], - ) -> std::result::Result { - match self { - #(#unpack_arms)* + /// Generate the IntoCTokenVariant implementation. + fn generate_into_ctoken_variant_impl(&self) -> TokenStream { + quote! { + impl light_sdk::compressible::IntoCTokenVariant for TokenAccountVariant { + fn into_ctoken_variant(self, token_data: light_token_sdk::compat::TokenData) -> RentFreeAccountVariant { + RentFreeAccountVariant::CTokenData(light_token_sdk::compat::CTokenData { + variant: self, + token_data, + }) } } } + } +} - impl light_sdk::compressible::IntoCTokenVariant for TokenAccountVariant { - fn into_ctoken_variant(self, token_data: light_token_sdk::compat::TokenData) -> RentFreeAccountVariant { - RentFreeAccountVariant::CTokenData(light_token_sdk::compat::CTokenData { - variant: self, - token_data, - }) +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +/// Extract ctx.* field names from seed elements (both token seeds and authority seeds). +/// +/// Uses the visitor-based FieldExtractor for clean AST traversal. +pub fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { + const EXCLUDED: &[&str] = &[ + "fee_payer", + "rent_sponsor", + "config", + "compression_authority", + ]; + + let mut all_fields = Vec::new(); + let mut seen = std::collections::HashSet::new(); + + for seed in spec.seeds.iter().chain(spec.authority.iter().flatten()) { + if let SeedElement::Expression(expr) = seed { + // Extract fields from this expression using the visitor + let fields = super::visitors::FieldExtractor::ctx_fields(EXCLUDED).extract(expr); + // Deduplicate across seeds + for field in fields { + let name = field.to_string(); + if seen.insert(name) { + all_fields.push(field); + } } } - }) + } + + all_fields } diff --git a/sdk-libs/macros/src/rentfree/program/visitors.rs b/sdk-libs/macros/src/rentfree/program/visitors.rs index f27a46a0d2..378a0522dc 100644 --- a/sdk-libs/macros/src/rentfree/program/visitors.rs +++ b/sdk-libs/macros/src/rentfree/program/visitors.rs @@ -494,7 +494,7 @@ pub fn generate_client_seed_code( } ClientSeedInfo::RawExpr(expr) => { - expressions.push(quote! { (#expr).as_ref() }); + expressions.push(quote! { { let __seed: &[u8] = (#expr).as_ref(); __seed } }); } } Ok(()) diff --git a/sdk-libs/sdk/src/account.rs b/sdk-libs/sdk/src/account.rs index 13050a649e..0a7f139fcc 100644 --- a/sdk-libs/sdk/src/account.rs +++ b/sdk-libs/sdk/src/account.rs @@ -139,7 +139,7 @@ use crate::{ const DEFAULT_DATA_HASH: [u8; 32] = [0u8; 32]; pub trait Size { - fn size(&self) -> usize; + fn size(&self) -> Result; } pub use sha::LightAccount; /// SHA256 borsh flat hashed Light Account. @@ -303,7 +303,7 @@ pub mod __internal { &self.owner } /// Get the byte size of the account type. - pub fn size(&self) -> usize + pub fn size(&self) -> Result where A: Size, { diff --git a/sdk-libs/sdk/src/compressible/compress_account.rs b/sdk-libs/sdk/src/compressible/compress_account.rs index 20ded890cc..a14bfcfcac 100644 --- a/sdk-libs/sdk/src/compressible/compress_account.rs +++ b/sdk-libs/sdk/src/compressible/compress_account.rs @@ -73,7 +73,7 @@ where let rent_exemption_lamports = Rent::get() .map_err(|_| LightSdkError::ConstraintViolation)? .minimum_balance(bytes as usize); - let ci = account_data.compression_info(); + let ci = account_data.compression_info()?; let last_claimed_slot = ci.last_claimed_slot(); let rent_cfg = ci.rent_config; let state = AccountRentState { @@ -100,7 +100,7 @@ where return Err(LightSdkError::ConstraintViolation.into()); } - account_data.compression_info_mut().set_compressed(); + account_data.compression_info_mut()?.set_compressed(); { let mut data = account_info .try_borrow_mut_data() diff --git a/sdk-libs/sdk/src/compressible/compress_account_on_init.rs b/sdk-libs/sdk/src/compressible/compress_account_on_init.rs index 24c46eb102..aeb8711464 100644 --- a/sdk-libs/sdk/src/compressible/compress_account_on_init.rs +++ b/sdk-libs/sdk/src/compressible/compress_account_on_init.rs @@ -82,17 +82,18 @@ where ); if with_data { - account_data.compression_info_mut().set_compressed(); + account_data.compression_info_mut()?.set_compressed(); } else { account_data - .compression_info_mut() + .compression_info_mut()? .bump_last_claimed_slot()?; } { let mut data = account_info .try_borrow_mut_data() .map_err(|_| LightSdkError::ConstraintViolation)?; - account_data.serialize(&mut &mut data[..]).map_err(|e| { + // Skip the 8-byte Anchor discriminator when serializing + account_data.serialize(&mut &mut data[8..]).map_err(|e| { msg!("Failed to serialize account data: {}", e); LightSdkError::ConstraintViolation })?; @@ -105,7 +106,7 @@ where if with_data { let mut compressed_data = account_data.clone(); - compressed_data.set_compression_info_none(); + compressed_data.set_compression_info_none()?; compressed_account.account = compressed_data; } else { compressed_account.remove_data(); diff --git a/sdk-libs/sdk/src/compressible/compress_runtime.rs b/sdk-libs/sdk/src/compressible/compress_runtime.rs index c5cb536452..79a3f0453f 100644 --- a/sdk-libs/sdk/src/compressible/compress_runtime.rs +++ b/sdk-libs/sdk/src/compressible/compress_runtime.rs @@ -4,6 +4,7 @@ use light_sdk_types::{ instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, CpiSigner, }; use solana_account_info::AccountInfo; +use solana_msg::msg; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; @@ -47,9 +48,19 @@ where crate::compressible::CompressibleConfig::load_checked(ctx.config(), program_id)?; if *ctx.rent_sponsor().key != compression_config.rent_sponsor { + msg!( + "invalid rent sponsor {:?} != {:?}, expected", + *ctx.rent_sponsor().key, + compression_config.rent_sponsor + ); return Err(ProgramError::Custom(0)); } if *ctx.compression_authority().key != compression_config.compression_authority { + msg!( + "invalid rent sponsor {:?} != {:?}, expected", + *ctx.compression_authority().key, + compression_config.compression_authority + ); return Err(ProgramError::Custom(0)); } diff --git a/sdk-libs/sdk/src/compressible/compression_info.rs b/sdk-libs/sdk/src/compressible/compression_info.rs index 3ffb42d6c9..da46041993 100644 --- a/sdk-libs/sdk/src/compressible/compression_info.rs +++ b/sdk-libs/sdk/src/compressible/compression_info.rs @@ -14,7 +14,7 @@ use crate::{instruction::PackedAccounts, AnchorDeserialize, AnchorSerialize, Pro pub trait Pack { type Packed: AnchorSerialize + Clone + std::fmt::Debug; - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Result; } pub trait Unpack { @@ -34,10 +34,10 @@ pub enum AccountState { } pub trait HasCompressionInfo { - fn compression_info(&self) -> &CompressionInfo; - fn compression_info_mut(&mut self) -> &mut CompressionInfo; + fn compression_info(&self) -> Result<&CompressionInfo, ProgramError>; + fn compression_info_mut(&mut self) -> Result<&mut CompressionInfo, ProgramError>; fn compression_info_mut_opt(&mut self) -> &mut Option; - fn set_compression_info_none(&mut self); + fn set_compression_info_none(&mut self) -> Result<(), ProgramError>; } /// Account space when compressed. @@ -266,7 +266,7 @@ where .map_err(|_| ProgramError::Custom(0))? .minimum_balance(bytes as usize); - let ci = account_data.compression_info_mut(); + let ci = account_data.compression_info_mut()?; let state = AccountRentState { num_bytes: bytes, current_slot, diff --git a/sdk-libs/sdk/src/compressible/decompress_idempotent.rs b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs index 831e915c75..d4512bfacb 100644 --- a/sdk-libs/sdk/src/compressible/decompress_idempotent.rs +++ b/sdk-libs/sdk/src/compressible/decompress_idempotent.rs @@ -115,7 +115,7 @@ where // Account space needs to include discriminator + serialized data // T::size() already includes the full Option footprint let discriminator_len = T::LIGHT_DISCRIMINATOR.len(); - let space = discriminator_len + T::size(&light_account.account); + let space = discriminator_len + T::size(&light_account.account)?; let rent_minimum_balance = rent.minimum_balance(space); invoke_create_account_with_heap( diff --git a/sdk-libs/sdk/src/error.rs b/sdk-libs/sdk/src/error.rs index 3dddbd1eec..d47c5bb1e6 100644 --- a/sdk-libs/sdk/src/error.rs +++ b/sdk-libs/sdk/src/error.rs @@ -107,6 +107,14 @@ pub enum LightSdkError { ExpectedSelfProgram, #[error("Expected CPI context to be provided")] ExpectedCpiContext, + #[error("Account missing compression_info field")] + MissingCompressionInfo, + #[error("Cannot access compression_info on packed variant")] + PackedVariantCompressionInfo, + #[error("CToken variant does not support compression_info operations")] + CTokenCompressionInfo, + #[error("Unexpected unpacked variant during decompression")] + UnexpectedUnpackedVariant, } impl From for ProgramError { @@ -196,6 +204,10 @@ impl From for u32 { LightSdkError::ExpectedTreeInfo => 16041, LightSdkError::ExpectedSelfProgram => 16042, LightSdkError::ExpectedCpiContext => 16043, + LightSdkError::MissingCompressionInfo => 16044, + LightSdkError::PackedVariantCompressionInfo => 16045, + LightSdkError::CTokenCompressionInfo => 16046, + LightSdkError::UnexpectedUnpackedVariant => 16047, } } } diff --git a/sdk-libs/token-sdk/src/compressed_token/v2/mint_action/account_metas.rs b/sdk-libs/token-sdk/src/compressed_token/v2/mint_action/account_metas.rs index 013af615d8..e04b9eda11 100644 --- a/sdk-libs/token-sdk/src/compressed_token/v2/mint_action/account_metas.rs +++ b/sdk-libs/token-sdk/src/compressed_token/v2/mint_action/account_metas.rs @@ -48,6 +48,7 @@ impl MintActionMetaConfig { } /// Create a new MintActionMetaConfig for operations on an existing compressed mint. + #[inline(never)] pub fn new( fee_payer: Pubkey, authority: Pubkey, @@ -139,6 +140,7 @@ impl MintActionMetaConfig { /// Get the account metas for a mint action instruction #[profile] + #[inline(never)] pub fn to_account_metas(self) -> Vec { let default_pubkeys = TokenDefaultAccounts::default(); let mut metas = Vec::new(); diff --git a/sdk-libs/token-sdk/src/lib.rs b/sdk-libs/token-sdk/src/lib.rs index d80cd22760..4f36bb77c6 100644 --- a/sdk-libs/token-sdk/src/lib.rs +++ b/sdk-libs/token-sdk/src/lib.rs @@ -78,4 +78,8 @@ use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSeria pub use light_compressed_account::instruction_data::compressed_proof::{ CompressedProof, ValidityProof, }; +pub use light_token_interface::{ + instructions::extensions::{ExtensionInstructionData, TokenMetadataInstructionData}, + state::AdditionalMetadata, +}; pub use pack::compat; diff --git a/sdk-libs/token-sdk/src/pack.rs b/sdk-libs/token-sdk/src/pack.rs index 43b0810b4d..0a357dc2bb 100644 --- a/sdk-libs/token-sdk/src/pack.rs +++ b/sdk-libs/token-sdk/src/pack.rs @@ -16,7 +16,7 @@ use crate::{AnchorDeserialize, AnchorSerialize}; // The sdk has identical trait definitions in light_sdk::compressible. pub trait Pack { type Packed; - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Result; } pub trait Unpack { type Unpacked; @@ -150,8 +150,11 @@ pub mod compat { impl Pack for TokenData { type Packed = InputTokenDataCompressible; - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - InputTokenDataCompressible { + fn pack( + &self, + remaining_accounts: &mut PackedAccounts, + ) -> Result { + Ok(InputTokenDataCompressible { owner: remaining_accounts.insert_or_get(self.owner), mint: remaining_accounts.insert_or_get_read_only(self.mint), amount: self.amount, @@ -162,7 +165,7 @@ pub mod compat { 0 }, version: TokenDataVersion::ShaFlat as u8, - } + }) } } @@ -236,11 +239,14 @@ pub mod compat { { type Packed = PackedTokenDataWithVariant; - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - PackedTokenDataWithVariant { - variant: self.variant.pack(remaining_accounts), - token_data: self.token_data.pack(remaining_accounts), - } + fn pack( + &self, + remaining_accounts: &mut PackedAccounts, + ) -> Result { + Ok(PackedTokenDataWithVariant { + variant: self.variant.pack(remaining_accounts)?, + token_data: self.token_data.pack(remaining_accounts)?, + }) } } @@ -270,11 +276,14 @@ pub mod compat { { type Packed = PackedTokenDataWithVariant; - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - PackedTokenDataWithVariant { - variant: self.variant.pack(remaining_accounts), - token_data: self.token_data.pack(remaining_accounts), - } + fn pack( + &self, + remaining_accounts: &mut PackedAccounts, + ) -> Result { + Ok(PackedTokenDataWithVariant { + variant: self.variant.pack(remaining_accounts)?, + token_data: self.token_data.pack(remaining_accounts)?, + }) } } diff --git a/sdk-libs/token-sdk/src/token/create_mints.rs b/sdk-libs/token-sdk/src/token/create_mints.rs index 8ffa836fe5..fcfd1a15f9 100644 --- a/sdk-libs/token-sdk/src/token/create_mints.rs +++ b/sdk-libs/token-sdk/src/token/create_mints.rs @@ -12,9 +12,12 @@ use light_batched_merkle_tree::queue::BatchedQueueAccount; use light_compressed_account::instruction_data::traits::LightInstructionData; use light_token_interface::{ - instructions::mint_action::{ - Action, CpiContext, DecompressMintAction, MintActionCompressedInstructionData, - MintInstructionData, + instructions::{ + extensions::{ExtensionInstructionData, TokenMetadataInstructionData}, + mint_action::{ + Action, CpiContext, CreateMint, DecompressMintAction, + MintActionCompressedInstructionData, MintInstructionData, + }, }, state::MintMetadata, LIGHT_TOKEN_PROGRAM_ID, @@ -53,6 +56,8 @@ pub struct SingleMintParams<'a> { pub authority_seeds: Option<&'a [&'a [u8]]>, /// Optional mint signer seeds for PDA signing pub mint_signer_seeds: Option<&'a [&'a [u8]]>, + /// Optional token metadata for the mint (reference to avoid stack overflow) + pub token_metadata: Option<&'a TokenMetadataInstructionData>, } /// Parameters for creating one or more compressed mints with decompression. @@ -89,6 +94,7 @@ pub struct CreateMintsParams<'a> { } impl<'a> CreateMintsParams<'a> { + #[inline(never)] pub fn new( mints: &'a [SingleMintParams<'a>], proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof, @@ -195,6 +201,7 @@ pub struct CreateMintsCpi<'a, 'info> { impl<'a, 'info> CreateMintsCpi<'a, 'info> { /// Validate that the struct is properly constructed. + #[inline(never)] pub fn validate(&self) -> Result<(), ProgramError> { let n = self.params.mints.len(); if n == 0 { @@ -213,6 +220,7 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { /// /// Signer seeds are extracted from `SingleMintParams::mint_signer_seeds` and /// `SingleMintParams::authority_seeds` for each CPI call (0, 1, or 2 seeds per call). + #[inline(never)] pub fn invoke(self) -> Result<(), ProgramError> { self.validate()?; let n = self.params.mints.len(); @@ -382,28 +390,31 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { let mint_params = &self.params.mints[last_idx]; let offset = self.params.cpi_context_offset; - let execute_cpi_context = CpiContext { - set_context: false, - first_set_context: false, - in_tree_index: self.params.address_tree_index, - in_queue_index: self.params.address_tree_index, // CPI context queue index - out_queue_index: self.params.output_queue_index, - token_out_queue_index: 0, - assigned_account_index: offset + last_idx as u8, - read_only_address_trees: [0; 4], - address_tree_pubkey: self.address_tree.key.to_bytes(), - }; - let mint_data = build_mint_instruction_data(mint_params, self.mint_seed_accounts[last_idx].key); - let instruction_data = MintActionCompressedInstructionData::new_mint( - mint_params.address_merkle_tree_root_index, - self.params.proof, - mint_data, - ) - .with_cpi_context(execute_cpi_context) - .with_decompress_mint(*decompress_action); + // Create struct directly to reduce stack usage (avoid builder pattern intermediates) + let instruction_data = MintActionCompressedInstructionData { + leaf_index: 0, + prove_by_index: false, + root_index: mint_params.address_merkle_tree_root_index, + max_top_up: 0, + create_mint: Some(CreateMint::default()), + actions: vec![Action::DecompressMint(*decompress_action)], + proof: Some(self.params.proof), + cpi_context: Some(CpiContext { + set_context: false, + first_set_context: false, + in_tree_index: self.params.address_tree_index, + in_queue_index: self.params.address_tree_index, + out_queue_index: self.params.output_queue_index, + token_out_queue_index: 0, + assigned_account_index: offset + last_idx as u8, + read_only_address_trees: [0; 4], + address_tree_pubkey: self.address_tree.key.to_bytes(), + }), + mint: Some(mint_data), + }; let mut meta_config = MintActionMetaConfig::new_create_mint( *self.payer.key, @@ -539,6 +550,12 @@ fn build_mint_instruction_data( mint_params: &SingleMintParams<'_>, mint_signer: &Pubkey, ) -> MintInstructionData { + // Convert token_metadata to extensions if present + let extensions = mint_params + .token_metadata + .cloned() + .map(|metadata| vec![ExtensionInstructionData::TokenMetadata(metadata)]); + MintInstructionData { supply: 0, decimals: mint_params.decimals, @@ -551,7 +568,7 @@ fn build_mint_instruction_data( }, mint_authority: Some(mint_params.mint_authority.to_bytes().into()), freeze_authority: mint_params.freeze_authority.map(|a| a.to_bytes().into()), - extensions: None, + extensions, } } @@ -586,6 +603,7 @@ fn get_base_leaf_index(output_queue: &AccountInfo) -> Result /// - `[N+11]`: rent_sponsor (writable) /// - `[N+12..2N+12]`: mint_pdas (writable) /// - `[2N+12]`: compressed_token_program (for CPI) +#[inline(never)] pub fn create_mints<'a, 'info>( payer: &AccountInfo<'info>, accounts: &'info [AccountInfo<'info>], @@ -611,7 +629,7 @@ pub fn create_mints<'a, 'info>( let mint_pdas_start = n + 12; // Build named struct from accounts slice - CreateMintsCpi { + let cpi = CreateMintsCpi { mint_seed_accounts: &accounts[mint_signers_start..mint_signers_start + n], payer: payer.clone(), address_tree: accounts[address_tree_idx].clone(), @@ -630,8 +648,8 @@ pub fn create_mints<'a, 'info>( }, cpi_context_account: accounts[cpi_context_idx].clone(), params, - } - .invoke() + }; + cpi.invoke() } // // ============================================================================ diff --git a/sdk-libs/token-sdk/src/token/mod.rs b/sdk-libs/token-sdk/src/token/mod.rs index b646594031..82e3676d88 100644 --- a/sdk-libs/token-sdk/src/token/mod.rs +++ b/sdk-libs/token-sdk/src/token/mod.rs @@ -134,10 +134,10 @@ pub use freeze::*; use light_compressible::config::CompressibleConfig; pub use light_token_interface::{ instructions::{ - extensions::{CompressToPubkey, ExtensionInstructionData}, + extensions::{CompressToPubkey, ExtensionInstructionData, TokenMetadataInstructionData}, mint_action::MintWithContext, }, - state::{Token, TokenDataVersion}, + state::{AdditionalMetadata, Token, TokenDataVersion}, }; use light_token_types::POOL_SEED; pub use mint_to::{MintTo, MintToCpi}; diff --git a/sdk-libs/token-sdk/tests/pack_test.rs b/sdk-libs/token-sdk/tests/pack_test.rs index 7c52d45c53..7bf8fb8d30 100644 --- a/sdk-libs/token-sdk/tests/pack_test.rs +++ b/sdk-libs/token-sdk/tests/pack_test.rs @@ -25,7 +25,7 @@ fn test_token_data_packing() { }; // Pack the token data - let packed = token_data.pack(&mut remaining_accounts); + let packed = token_data.pack(&mut remaining_accounts).unwrap(); // Verify the packed data assert_eq!(packed.owner, 0); // First pubkey gets index 0 @@ -55,8 +55,11 @@ fn test_token_data_with_variant_packing() { impl Pack for MyVariant { type Packed = Self; - fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { - *self + fn pack( + &self, + _remaining_accounts: &mut PackedAccounts, + ) -> Result { + Ok(*self) } } @@ -76,7 +79,7 @@ fn test_token_data_with_variant_packing() { // Pack the wrapper let packed: PackedCompressibleTokenDataWithVariant = - token_with_variant.pack(&mut remaining_accounts); + token_with_variant.pack(&mut remaining_accounts).unwrap(); // Verify variant is unchanged assert!(matches!(packed.variant, MyVariant::TypeA)); @@ -114,8 +117,8 @@ fn test_deduplication_in_packing() { }; // Pack both tokens - let packed1 = token1.pack(&mut remaining_accounts); - let packed2 = token2.pack(&mut remaining_accounts); + let packed1 = token1.pack(&mut remaining_accounts).unwrap(); + let packed2 = token2.pack(&mut remaining_accounts).unwrap(); // Both should reference the same indices assert_eq!(packed1.owner, packed2.owner); diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs index 5c7185f4fb..a486eae760 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs @@ -194,25 +194,23 @@ pub struct CreateTwoMints<'info> { } // ============================================================================= -// Four Mints Test +// Three Mints Test // ============================================================================= pub const MINT_SIGNER_C_SEED: &[u8] = b"mint_signer_c"; -pub const MINT_SIGNER_D_SEED: &[u8] = b"mint_signer_d"; #[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct CreateFourMintsParams { +pub struct CreateThreeMintsParams { pub create_accounts_proof: CreateAccountsProof, pub mint_signer_a_bump: u8, pub mint_signer_b_bump: u8, pub mint_signer_c_bump: u8, - pub mint_signer_d_bump: u8, } -/// Test instruction with 4 #[light_mint] fields to verify multi-mint support. +/// Test instruction with 3 #[light_mint] fields to verify multi-mint support. #[derive(Accounts, RentFree)] -#[instruction(params: CreateFourMintsParams)] -pub struct CreateFourMints<'info> { +#[instruction(params: CreateThreeMintsParams)] +pub struct CreateThreeMints<'info> { #[account(mut)] pub fee_payer: Signer<'info>, @@ -239,13 +237,6 @@ pub struct CreateFourMints<'info> { )] pub mint_signer_c: UncheckedAccount<'info>, - /// CHECK: PDA derived from authority for mint D - #[account( - seeds = [MINT_SIGNER_D_SEED, authority.key().as_ref()], - bump, - )] - pub mint_signer_d: UncheckedAccount<'info>, - /// CHECK: Initialized by light_mint CPI #[account(mut)] #[light_mint( @@ -276,15 +267,72 @@ pub struct CreateFourMints<'info> { )] pub cmint_c: UncheckedAccount<'info>, - /// CHECK: Initialized by light_mint CPI + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + /// CHECK: CToken config + pub ctoken_compressible_config: AccountInfo<'info>, + + /// CHECK: CToken rent sponsor + #[account(mut)] + pub ctoken_rent_sponsor: AccountInfo<'info>, + + /// CHECK: CToken program + pub light_token_program: AccountInfo<'info>, + + /// CHECK: CToken CPI authority + pub ctoken_cpi_authority: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================= +// Mint With Metadata Test +// ============================================================================= + +pub const METADATA_MINT_SIGNER_SEED: &[u8] = b"metadata_mint_signer"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateMintWithMetadataParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_bump: u8, + pub name: Vec, + pub symbol: Vec, + pub uri: Vec, + pub additional_metadata: Option>, +} + +/// Test instruction with #[light_mint] with metadata fields. +/// Tests the metadata support in the RentFree macro. +#[derive(Accounts, RentFree)] +#[instruction(params: CreateMintWithMetadataParams)] +pub struct CreateMintWithMetadata<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + /// CHECK: PDA derived from authority for mint signer + #[account( + seeds = [METADATA_MINT_SIGNER_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer: UncheckedAccount<'info>, + + /// CHECK: Initialized by light_mint CPI with metadata #[account(mut)] #[light_mint( - mint_signer = mint_signer_d, + mint_signer = mint_signer, authority = fee_payer, - decimals = 12, - mint_seeds = &[MINT_SIGNER_D_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_d_bump]] + decimals = 9, + mint_seeds = &[METADATA_MINT_SIGNER_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_bump]], + name = params.name.clone(), + symbol = params.symbol.clone(), + uri = params.uri.clone(), + update_authority = authority, + additional_metadata = params.additional_metadata.clone() )] - pub cmint_d: UncheckedAccount<'info>, + pub cmint: UncheckedAccount<'info>, /// CHECK: Compression config pub compression_config: AccountInfo<'info>, diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/array_bumps.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/array_bumps.rs new file mode 100644 index 0000000000..f8f599b9b1 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/array_bumps.rs @@ -0,0 +1,215 @@ +//! D9 Test: Various seed patterns with bump +//! +//! Tests seed combinations that use the `bump` attribute +//! Note: The actual bump byte is handled by Anchor's `bump` attribute, +//! not by including &[bump] in the seeds array. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +/// Constant for bump tests +pub const D9_BUMP_SEED: &[u8] = b"d9_bump"; + +/// String constant for .as_bytes() test +pub const D9_BUMP_STR: &str = "d9_bump_str"; + +// ============================================================================ +// Test 1: Literal seed with bump +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9BumpLiteralParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests literal seed with bump attribute +#[derive(Accounts, RentFree)] +#[instruction(params: D9BumpLiteralParams)] +pub struct D9BumpLiteral<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_bump_lit"], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 2: Constant seed with bump +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9BumpConstantParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests constant seed with bump attribute +#[derive(Accounts, RentFree)] +#[instruction(params: D9BumpConstantParams)] +pub struct D9BumpConstant<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_BUMP_SEED], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 3: Qualified path with .as_bytes() and bump +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9BumpQualifiedParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests crate::path::CONST.as_bytes() with bump - the pattern that caused type inference issues +#[derive(Accounts, RentFree)] +#[instruction(params: D9BumpQualifiedParams)] +pub struct D9BumpQualified<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [crate::instructions::d9_seeds::array_bumps::D9_BUMP_STR.as_bytes()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 4: Param seed with bump +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9BumpParamParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests params.owner.as_ref() with bump +#[derive(Accounts, RentFree)] +#[instruction(params: D9BumpParamParams)] +pub struct D9BumpParam<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_bump_param", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 5: Ctx account seed with bump +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9BumpCtxParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests account.key().as_ref() with bump +#[derive(Accounts, RentFree)] +#[instruction(params: D9BumpCtxParams)] +pub struct D9BumpCtx<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Authority account + pub authority: AccountInfo<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_bump_ctx", authority.key().as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 6: Multiple seed types with bump +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9BumpMixedParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, + pub id: u64, +} + +/// Tests literal + constant + param + bytes with bump +#[derive(Accounts, RentFree)] +#[instruction(params: D9BumpMixedParams)] +pub struct D9BumpMixed<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_bump_mix", D9_BUMP_SEED, params.owner.as_ref(), params.id.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/complex_mixed.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/complex_mixed.rs new file mode 100644 index 0000000000..800b6b32c1 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/complex_mixed.rs @@ -0,0 +1,292 @@ +//! D9 Test: Complex multi-seed combinations +//! +//! Tests multi-seed combinations with 3+ seeds: +//! - Various type combinations +//! - Different orderings +//! - Maximum seed complexity + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +/// Constants for complex tests +pub const D9_COMPLEX_V1: &[u8] = b"v1"; +pub const D9_COMPLEX_PREFIX: &[u8] = b"prefix"; +pub const D9_COMPLEX_NAMESPACE: &str = "namespace"; + +// ============================================================================ +// Test 1: Three seeds - literal + constant + param +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ComplexThreeParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests 3 seeds: literal + constant + param.as_ref() +#[derive(Accounts, RentFree)] +#[instruction(params: D9ComplexThreeParams)] +pub struct D9ComplexThree<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_complex3", D9_COMPLEX_PREFIX, params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 2: Four seeds - mixed types +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ComplexFourParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, + pub id: u64, +} + +/// Tests 4 seeds: version + namespace + param + bytes +#[derive(Accounts, RentFree)] +#[instruction(params: D9ComplexFourParams)] +pub struct D9ComplexFour<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_COMPLEX_V1, D9_COMPLEX_NAMESPACE.as_bytes(), params.owner.as_ref(), params.id.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 3: Five seeds - ctx account included +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ComplexFiveParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, + pub id: u64, +} + +/// Tests 5 seeds with context account +#[derive(Accounts, RentFree)] +#[instruction(params: D9ComplexFiveParams)] +pub struct D9ComplexFive<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Authority for seeds + pub authority: AccountInfo<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_COMPLEX_V1, D9_COMPLEX_NAMESPACE.as_bytes(), authority.key().as_ref(), params.owner.as_ref(), params.id.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 4: Qualified paths mixed with local +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ComplexQualifiedMixParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests qualified crate paths mixed with local constants +#[derive(Accounts, RentFree)] +#[instruction(params: D9ComplexQualifiedMixParams)] +pub struct D9ComplexQualifiedMix<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [crate::instructions::d9_seeds::complex_mixed::D9_COMPLEX_V1, D9_COMPLEX_PREFIX, params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 5: Function call + other seeds +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ComplexFuncParams { + pub create_accounts_proof: CreateAccountsProof, + pub key_a: Pubkey, + pub key_b: Pubkey, + pub id: u64, +} + +/// Tests function call combined with other seed types +#[derive(Accounts, RentFree)] +#[instruction(params: D9ComplexFuncParams)] +pub struct D9ComplexFunc<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_COMPLEX_V1, crate::max_key(¶ms.key_a, ¶ms.key_b).as_ref(), params.id.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 6: All qualified paths +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ComplexAllQualifiedParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests all paths being fully qualified +#[derive(Accounts, RentFree)] +#[instruction(params: D9ComplexAllQualifiedParams)] +pub struct D9ComplexAllQualified<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [ + crate::instructions::d9_seeds::complex_mixed::D9_COMPLEX_V1, + crate::instructions::d9_seeds::complex_mixed::D9_COMPLEX_NAMESPACE.as_bytes(), + params.owner.as_ref() + ], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 7: Static function (program ID) as seed +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ComplexProgramIdParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests using crate::ID (program ID) as a seed element +#[derive(Accounts, RentFree)] +#[instruction(params: D9ComplexProgramIdParams)] +pub struct D9ComplexProgramId<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_progid", crate::ID.as_ref(), params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 8: Static id() function call as seed +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ComplexIdFuncParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests using crate::id() function call as a seed element +#[derive(Accounts, RentFree)] +#[instruction(params: D9ComplexIdFuncParams)] +pub struct D9ComplexIdFunc<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_idfunc", crate::id().as_ref(), params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/const_patterns.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/const_patterns.rs new file mode 100644 index 0000000000..13ac5de813 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/const_patterns.rs @@ -0,0 +1,444 @@ +//! D9 Test: Constant and static patterns +//! +//! Tests various constant/static seed patterns: +//! - Associated constants: SomeStruct::SEED +//! - Const fn calls: const_fn() +//! - Const fn with generics: const_fn::() +//! - Trait associated constants: ::CONSTANT + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +// ============================================================================ +// Constants and types for testing +// ============================================================================ + +/// Struct with associated constant +pub struct SeedHolder; + +impl SeedHolder { + pub const SEED: &'static [u8] = b"holder_seed"; + pub const NAMESPACE: &'static str = "holder_ns"; +} + +/// Another struct with associated constant +pub struct AnotherHolder; + +impl AnotherHolder { + pub const PREFIX: &'static [u8] = b"another_prefix"; +} + +/// Trait with associated constant +pub trait HasSeed { + const TRAIT_SEED: &'static [u8]; +} + +impl HasSeed for SeedHolder { + const TRAIT_SEED: &'static [u8] = b"trait_seed"; +} + +/// Const fn returning bytes +pub const fn const_seed() -> &'static [u8] { + b"const_fn_seed" +} + +/// Const fn with generic (returns the input) +pub const fn identity_seed(seed: &'static [u8; N]) -> &'static [u8; N] { + seed +} + +/// Static seed value +pub static STATIC_SEED: [u8; 11] = *b"static_seed"; + +// ============================================================================ +// Test 1: Associated constant +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9AssocConstParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests SomeStruct::CONSTANT pattern +#[derive(Accounts, RentFree)] +#[instruction(params: D9AssocConstParams)] +pub struct D9AssocConst<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [SeedHolder::SEED], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 2: Associated constant with method +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9AssocConstMethodParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests SomeStruct::CONSTANT.as_bytes() pattern +#[derive(Accounts, RentFree)] +#[instruction(params: D9AssocConstMethodParams)] +pub struct D9AssocConstMethod<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [SeedHolder::NAMESPACE.as_bytes()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 3: Multiple associated constants +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9MultiAssocConstParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests multiple associated constants from different types +#[derive(Accounts, RentFree)] +#[instruction(params: D9MultiAssocConstParams)] +pub struct D9MultiAssocConst<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [SeedHolder::SEED, AnotherHolder::PREFIX, params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 4: Const fn call +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ConstFnParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests const_fn() pattern +#[derive(Accounts, RentFree)] +#[instruction(params: D9ConstFnParams)] +pub struct D9ConstFn<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [const_seed()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 5: Const fn with const generic +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ConstFnGenericParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests const_fn::() pattern with const generics +#[derive(Accounts, RentFree)] +#[instruction(params: D9ConstFnGenericParams)] +pub struct D9ConstFnGeneric<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [identity_seed::<12>(b"generic_seed")], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 6: Trait associated constant (fully qualified) +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9TraitAssocConstParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests ::CONSTANT pattern +#[derive(Accounts, RentFree)] +#[instruction(params: D9TraitAssocConstParams)] +pub struct D9TraitAssocConst<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [::TRAIT_SEED], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 7: Static variable +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9StaticParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests static variable as seed +#[derive(Accounts, RentFree)] +#[instruction(params: D9StaticParams)] +pub struct D9Static<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [&STATIC_SEED], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 8: Qualified const fn (crate path) +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9QualifiedConstFnParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests crate::module::const_fn() pattern +#[derive(Accounts, RentFree)] +#[instruction(params: D9QualifiedConstFnParams)] +pub struct D9QualifiedConstFn<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [crate::instructions::d9_seeds::const_patterns::const_seed()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 9: Fully qualified associated constant +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9FullyQualifiedAssocParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests crate::module::Type::CONSTANT pattern (fully qualified associated constant) +#[derive(Accounts, RentFree)] +#[instruction(params: D9FullyQualifiedAssocParams)] +pub struct D9FullyQualifiedAssoc<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [crate::instructions::d9_seeds::const_patterns::SeedHolder::SEED], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 10: Fully qualified trait associated constant +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9FullyQualifiedTraitParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests ::CONSTANT pattern +#[derive(Accounts, RentFree)] +#[instruction(params: D9FullyQualifiedTraitParams)] +pub struct D9FullyQualifiedTrait<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [::TRAIT_SEED], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 11: Fully qualified const fn with generics +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9FullyQualifiedGenericParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests crate::module::const_fn::() pattern +#[derive(Accounts, RentFree)] +#[instruction(params: D9FullyQualifiedGenericParams)] +pub struct D9FullyQualifiedGeneric<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [crate::instructions::d9_seeds::const_patterns::identity_seed::<10>(b"fq_generic")], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 12: Combined patterns with full qualification +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ConstCombinedParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests combining various constant patterns with full paths +#[derive(Accounts, RentFree)] +#[instruction(params: D9ConstCombinedParams)] +pub struct D9ConstCombined<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [ + crate::instructions::d9_seeds::const_patterns::SeedHolder::SEED, + crate::instructions::d9_seeds::const_patterns::const_seed(), + params.owner.as_ref() + ], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/edge_cases.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/edge_cases.rs new file mode 100644 index 0000000000..ccbda17d95 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/edge_cases.rs @@ -0,0 +1,253 @@ +//! D9 Test: Edge cases and boundary conditions +//! +//! Tests boundary conditions: +//! - Empty literal +//! - Single byte constant +//! - Single letter constant names +//! - Constant names with digits +//! - Leading underscore constants +//! - Many literals in same seeds array + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +/// Single letter constant +pub const A: &[u8] = b"a"; + +/// Constant with digits +pub const SEED_123: &[u8] = b"seed123"; + +/// Leading underscore constant +pub const _UNDERSCORE_CONST: &[u8] = b"underscore"; + +/// Single byte constant +pub const D9_SINGLE_BYTE: &[u8] = b"x"; + +// ============================================================================ +// Test 1: Minimal literal (single character) +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9EdgeEmptyParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests minimal byte literal seed +#[derive(Accounts, RentFree)] +#[instruction(params: D9EdgeEmptyParams)] +pub struct D9EdgeEmpty<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [&b"d9_edge_empty"[..], &b"_"[..], params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 2: Single byte constant +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9EdgeSingleByteParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests single byte constant +#[derive(Accounts, RentFree)] +#[instruction(params: D9EdgeSingleByteParams)] +pub struct D9EdgeSingleByte<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_SINGLE_BYTE], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 3: Single letter constant name +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9EdgeSingleLetterParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests single letter constant name (A) +#[derive(Accounts, RentFree)] +#[instruction(params: D9EdgeSingleLetterParams)] +pub struct D9EdgeSingleLetter<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [A], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 4: Constant name with digits +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9EdgeDigitsParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests constant name containing digits (SEED_123) +#[derive(Accounts, RentFree)] +#[instruction(params: D9EdgeDigitsParams)] +pub struct D9EdgeDigits<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [SEED_123], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 5: Leading underscore constant +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9EdgeUnderscoreParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests leading underscore constant name +#[derive(Accounts, RentFree)] +#[instruction(params: D9EdgeUnderscoreParams)] +pub struct D9EdgeUnderscore<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [_UNDERSCORE_CONST], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 6: Many literals in same seeds array +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9EdgeManyLiteralsParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests many byte literals in same seeds array +#[derive(Accounts, RentFree)] +#[instruction(params: D9EdgeManyLiteralsParams)] +pub struct D9EdgeManyLiterals<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"a", b"b", b"c", b"d", b"e"], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 7: Mixed edge cases in one struct +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9EdgeMixedParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests mixing various edge case constants +#[derive(Accounts, RentFree)] +#[instruction(params: D9EdgeMixedParams)] +pub struct D9EdgeMixed<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [A, SEED_123, _UNDERSCORE_CONST, params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/external_paths.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/external_paths.rs new file mode 100644 index 0000000000..622755fe66 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/external_paths.rs @@ -0,0 +1,219 @@ +//! D9 Test: External crate path variations +//! +//! Tests paths from external crates: +//! - light_sdk_types::constants::* +//! - light_token_types::constants::* +//! - Complex nested external paths + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +// ============================================================================ +// Test 1: External crate constant (light_sdk_types) +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ExternalSdkTypesParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests external crate path: light_sdk_types::constants::CPI_AUTHORITY_PDA_SEED +#[derive(Accounts, RentFree)] +#[instruction(params: D9ExternalSdkTypesParams)] +pub struct D9ExternalSdkTypes<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_ext_sdk", light_sdk_types::constants::CPI_AUTHORITY_PDA_SEED, params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 2: External crate constant (light_token_types) +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ExternalCtokenParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests external crate path: light_token_types::constants::POOL_SEED +#[derive(Accounts, RentFree)] +#[instruction(params: D9ExternalCtokenParams)] +pub struct D9ExternalCtoken<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_ext_ctoken", light_token_types::constants::POOL_SEED, params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 3: Multiple external crate constants mixed +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ExternalMixedParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests multiple external crate constants mixed together +#[derive(Accounts, RentFree)] +#[instruction(params: D9ExternalMixedParams)] +pub struct D9ExternalMixed<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [ + light_sdk_types::constants::CPI_AUTHORITY_PDA_SEED, + light_token_types::constants::POOL_SEED, + params.owner.as_ref() + ], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 4: External constant with local constant +// ============================================================================ + +/// Local constant to mix with external +pub const D9_EXTERNAL_LOCAL: &[u8] = b"d9_ext_local"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ExternalWithLocalParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests external constant combined with local constant +#[derive(Accounts, RentFree)] +#[instruction(params: D9ExternalWithLocalParams)] +pub struct D9ExternalWithLocal<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_EXTERNAL_LOCAL, light_sdk_types::constants::RENT_SPONSOR_SEED, params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 5: External constant with bump +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ExternalBumpParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests external constant path with bump attribute +#[derive(Accounts, RentFree)] +#[instruction(params: D9ExternalBumpParams)] +pub struct D9ExternalBump<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [light_token_interface::COMPRESSED_MINT_SEED, params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 6: Re-exported external constant +// ============================================================================ + +/// Re-export from external crate for path testing +pub use light_sdk_types::constants::CPI_AUTHORITY_PDA_SEED as REEXPORTED_SEED; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ExternalReexportParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests re-exported external constant +#[derive(Accounts, RentFree)] +#[instruction(params: D9ExternalReexportParams)] +pub struct D9ExternalReexport<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [REEXPORTED_SEED], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/method_chains.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/method_chains.rs new file mode 100644 index 0000000000..3d5e5b2b58 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/method_chains.rs @@ -0,0 +1,216 @@ +//! D9 Test: Method call variations +//! +//! Tests different method call patterns on seeds: +//! - .as_ref() on Pubkey +//! - .as_bytes() on string constants +//! - .to_le_bytes().as_ref() on numeric types +//! - .to_be_bytes().as_ref() on numeric types +//! - Method chains on qualified paths + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +/// String constant for .as_bytes() testing +pub const D9_METHOD_STR: &str = "d9_method_str"; + +/// Byte slice constant for .as_ref() testing +pub const D9_METHOD_BYTES: &[u8] = b"d9_method_bytes"; + +// ============================================================================ +// Test 1: Constant with .as_ref() +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9MethodAsRefParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests constant.as_ref() method call +#[derive(Accounts, RentFree)] +#[instruction(params: D9MethodAsRefParams)] +pub struct D9MethodAsRef<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_METHOD_BYTES.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 2: String constant with .as_bytes() +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9MethodAsBytesParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests string_constant.as_bytes() method call +#[derive(Accounts, RentFree)] +#[instruction(params: D9MethodAsBytesParams)] +pub struct D9MethodAsBytes<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_METHOD_STR.as_bytes()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 3: Qualified path with .as_bytes() +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9MethodQualifiedAsBytesParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests crate::path::CONST.as_bytes() - the pattern that caused type inference issues +#[derive(Accounts, RentFree)] +#[instruction(params: D9MethodQualifiedAsBytesParams)] +pub struct D9MethodQualifiedAsBytes<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [crate::instructions::d9_seeds::method_chains::D9_METHOD_STR.as_bytes()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 4: Param with .to_le_bytes().as_ref() +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9MethodToLeBytesParams { + pub create_accounts_proof: CreateAccountsProof, + pub id: u64, +} + +/// Tests params.field.to_le_bytes().as_ref() chain +#[derive(Accounts, RentFree)] +#[instruction(params: D9MethodToLeBytesParams)] +pub struct D9MethodToLeBytes<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_le", params.id.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 5: Param with .to_be_bytes().as_ref() +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9MethodToBeBytesParams { + pub create_accounts_proof: CreateAccountsProof, + pub id: u64, +} + +/// Tests params.field.to_be_bytes().as_ref() chain +#[derive(Accounts, RentFree)] +#[instruction(params: D9MethodToBeBytesParams)] +pub struct D9MethodToBeBytes<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_be", params.id.to_be_bytes().as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 6: Mixed methods in same seeds array +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9MethodMixedParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, + pub id: u64, +} + +/// Tests mixing different method calls in same seeds +#[derive(Accounts, RentFree)] +#[instruction(params: D9MethodMixedParams)] +pub struct D9MethodMixed<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_METHOD_STR.as_bytes(), params.owner.as_ref(), params.id.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mod.rs index e34cfa99bf..14b8852a71 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mod.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mod.rs @@ -7,21 +7,45 @@ //! - DataField (param): params.owner.as_ref() //! - DataField (bytes): params.id.to_le_bytes() //! - FunctionCall: max_key(&a, &b) +//! +//! Extended tests: +//! - Qualified paths: crate::, self::, external crate paths +//! - Method chains: as_ref(), as_bytes(), to_le_bytes(), to_be_bytes() +//! - Array bumps: &[params.bump] patterns +//! - Complex mixed: 3+ seeds, function calls, program ID +//! - Edge cases: empty literals, single byte, special names +//! - External paths: light_sdk_types, light_ctoken_types mod all; +pub mod array_bumps; +pub mod complex_mixed; +pub mod const_patterns; mod constant; mod ctx_account; +pub mod edge_cases; +pub mod external_paths; mod function_call; mod literal; +pub mod method_chains; mod mixed; +pub mod nested_seeds; mod param; mod param_bytes; +pub mod qualified_paths; pub use all::*; +pub use array_bumps::*; +pub use complex_mixed::*; +pub use const_patterns::*; pub use constant::*; pub use ctx_account::*; +pub use edge_cases::*; +pub use external_paths::*; pub use function_call::*; pub use literal::*; +pub use method_chains::*; pub use mixed::*; +pub use nested_seeds::*; pub use param::*; pub use param_bytes::*; +pub use qualified_paths::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/nested_seeds.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/nested_seeds.rs new file mode 100644 index 0000000000..7391f769c5 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/nested_seeds.rs @@ -0,0 +1,233 @@ +//! D9 Test: Nested seed expressions +//! +//! Tests deeply nested seed access patterns: +//! - params.nested.field access +//! - params.array[index].as_slice() array indexing +//! - Complex nested struct paths + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +// ============================================================================ +// Nested structs for testing +// ============================================================================ + +/// Inner nested struct +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct InnerNested { + pub owner: Pubkey, + pub id: u64, +} + +/// Outer nested struct containing inner +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct OuterNested { + pub array: [u8; 16], + pub nested: InnerNested, +} + +// ============================================================================ +// Test 1: Simple nested struct access +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9NestedSimpleParams { + pub create_accounts_proof: CreateAccountsProof, + pub nested: InnerNested, +} + +/// Tests params.nested.owner.as_ref() pattern +#[derive(Accounts, RentFree)] +#[instruction(params: D9NestedSimpleParams)] +pub struct D9NestedSimple<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_nested_simple", params.nested.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 2: Double nested struct access +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9NestedDoubleParams { + pub create_accounts_proof: CreateAccountsProof, + pub outer: OuterNested, +} + +/// Tests params.outer.nested.owner.as_ref() pattern (double nested) +#[derive(Accounts, RentFree)] +#[instruction(params: D9NestedDoubleParams)] +pub struct D9NestedDouble<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_nested_double", params.outer.nested.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 3: Nested array field access +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9NestedArrayFieldParams { + pub create_accounts_proof: CreateAccountsProof, + pub outer: OuterNested, +} + +/// Tests params.outer.array as seed (array field in nested struct) +#[derive(Accounts, RentFree)] +#[instruction(params: D9NestedArrayFieldParams)] +pub struct D9NestedArrayField<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_nested_array", params.outer.array.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 4: Array indexing - params.arrays[2].as_slice() +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ArrayIndexParams { + pub create_accounts_proof: CreateAccountsProof, + /// 2D array: 10 arrays of 16 bytes each + pub arrays: [[u8; 16]; 10], +} + +/// Tests params.arrays[2].as_slice() pattern (array indexing) +#[derive(Accounts, RentFree)] +#[instruction(params: D9ArrayIndexParams)] +pub struct D9ArrayIndex<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_array_idx", params.arrays[2].as_slice()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 5: Combined nested struct + bytes conversion +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9NestedBytesParams { + pub create_accounts_proof: CreateAccountsProof, + pub nested: InnerNested, +} + +/// Tests params.nested.id.to_le_bytes().as_ref() pattern +#[derive(Accounts, RentFree)] +#[instruction(params: D9NestedBytesParams)] +pub struct D9NestedBytes<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_nested_bytes", params.nested.id.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 6: Multiple nested seeds combined +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9NestedCombinedParams { + pub create_accounts_proof: CreateAccountsProof, + pub outer: OuterNested, +} + +/// Tests combining multiple nested accessors in seeds array +#[derive(Accounts, RentFree)] +#[instruction(params: D9NestedCombinedParams)] +pub struct D9NestedCombined<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [ + b"d9_nested_combined", + params.outer.array.as_ref(), + params.outer.nested.owner.as_ref() + ], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/qualified_paths.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/qualified_paths.rs new file mode 100644 index 0000000000..b7b378db40 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/qualified_paths.rs @@ -0,0 +1,180 @@ +//! D9 Test: Qualified path variations for constants +//! +//! Tests different path qualification styles for constant seeds: +//! - Bare constant: CONST +//! - crate:: prefix: crate::CONST +//! - self:: prefix: self::CONST +//! - Nested module paths + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +/// Local constant for self:: prefix testing +pub const D9_QUALIFIED_LOCAL: &[u8] = b"d9_qualified_local"; + +/// Constant for crate:: prefix testing (re-exports from lib.rs) +pub const D9_QUALIFIED_CRATE: &[u8] = b"d9_qualified_crate"; + +// ============================================================================ +// Test 1: Bare constant (no path prefix) +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9QualifiedBareParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests bare constant reference without path prefix +#[derive(Accounts, RentFree)] +#[instruction(params: D9QualifiedBareParams)] +pub struct D9QualifiedBare<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_QUALIFIED_LOCAL], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 2: self:: prefix +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9QualifiedSelfParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests self:: prefix path qualification +#[derive(Accounts, RentFree)] +#[instruction(params: D9QualifiedSelfParams)] +pub struct D9QualifiedSelf<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [self::D9_QUALIFIED_LOCAL], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 3: crate:: prefix +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9QualifiedCrateParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests crate:: prefix path qualification +#[derive(Accounts, RentFree)] +#[instruction(params: D9QualifiedCrateParams)] +pub struct D9QualifiedCrate<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [crate::instructions::d9_seeds::qualified_paths::D9_QUALIFIED_CRATE], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 4: Deep nested path +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9QualifiedDeepParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests deeply nested crate path +#[derive(Accounts, RentFree)] +#[instruction(params: D9QualifiedDeepParams)] +pub struct D9QualifiedDeep<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [crate::instructions::d9_seeds::D9_CONSTANT_SEED], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================ +// Test 5: Mixed qualified and bare in same seeds array +// ============================================================================ + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9QualifiedMixedParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests mixing qualified and bare paths in same seeds +#[derive(Accounts, RentFree)] +#[instruction(params: D9QualifiedMixedParams)] +pub struct D9QualifiedMixed<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_QUALIFIED_LOCAL, crate::instructions::d9_seeds::D9_CONSTANT_SEED, params.owner.as_ref()], + bump, + )] + #[rentfree] + pub record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs index 3da00b3738..102444ab80 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -1,4 +1,5 @@ #![allow(deprecated)] +#![allow(clippy::useless_asref)] // Testing macro handling of .as_ref() patterns use anchor_lang::prelude::*; use light_sdk::{derive_light_cpi_signer, derive_light_rent_sponsor_pda}; @@ -100,13 +101,147 @@ pub mod csdk_anchor_full_derived_test { D8All, D8AllParams, D8MultiRentfree, D8MultiRentfreeParams, D8PdaOnly, D8PdaOnlyParams, }, d9_seeds::{ - D9All, D9AllParams, D9Constant, D9ConstantParams, D9CtxAccount, D9CtxAccountParams, - D9FunctionCall, D9FunctionCallParams, D9Literal, D9LiteralParams, D9Mixed, - D9MixedParams, D9Param, D9ParamBytes, D9ParamBytesParams, D9ParamParams, + // Original tests + D9All, + D9AllParams, + D9ArrayIndex, + D9ArrayIndexParams, + // Const patterns tests + D9AssocConst, + D9AssocConstMethod, + D9AssocConstMethodParams, + D9AssocConstParams, + D9BumpConstant, + D9BumpConstantParams, + D9BumpCtx, + D9BumpCtxParams, + // Array bumps tests + D9BumpLiteral, + D9BumpLiteralParams, + D9BumpMixed, + D9BumpMixedParams, + D9BumpParam, + D9BumpParamParams, + D9BumpQualified, + D9BumpQualifiedParams, + D9ComplexAllQualified, + D9ComplexAllQualifiedParams, + D9ComplexFive, + D9ComplexFiveParams, + D9ComplexFour, + D9ComplexFourParams, + D9ComplexFunc, + D9ComplexFuncParams, + D9ComplexIdFunc, + D9ComplexIdFuncParams, + D9ComplexProgramId, + D9ComplexProgramIdParams, + D9ComplexQualifiedMix, + D9ComplexQualifiedMixParams, + // Complex mixed tests + D9ComplexThree, + D9ComplexThreeParams, + D9ConstCombined, + D9ConstCombinedParams, + D9ConstFn, + D9ConstFnGeneric, + D9ConstFnGenericParams, + D9ConstFnParams, + D9Constant, + D9ConstantParams, + D9CtxAccount, + D9CtxAccountParams, + D9EdgeDigits, + D9EdgeDigitsParams, + // Edge cases tests + D9EdgeEmpty, + D9EdgeEmptyParams, + D9EdgeManyLiterals, + D9EdgeManyLiteralsParams, + D9EdgeMixed, + D9EdgeMixedParams, + D9EdgeSingleByte, + D9EdgeSingleByteParams, + D9EdgeSingleLetter, + D9EdgeSingleLetterParams, + D9EdgeUnderscore, + D9EdgeUnderscoreParams, + D9ExternalBump, + D9ExternalBumpParams, + D9ExternalCtoken, + D9ExternalCtokenParams, + D9ExternalMixed, + D9ExternalMixedParams, + D9ExternalReexport, + D9ExternalReexportParams, + // External paths tests + D9ExternalSdkTypes, + D9ExternalSdkTypesParams, + D9ExternalWithLocal, + D9ExternalWithLocalParams, + D9FullyQualifiedAssoc, + D9FullyQualifiedAssocParams, + D9FullyQualifiedGeneric, + D9FullyQualifiedGenericParams, + D9FullyQualifiedTrait, + D9FullyQualifiedTraitParams, + D9FunctionCall, + D9FunctionCallParams, + D9Literal, + D9LiteralParams, + D9MethodAsBytes, + D9MethodAsBytesParams, + // Method chains tests + D9MethodAsRef, + D9MethodAsRefParams, + D9MethodMixed, + D9MethodMixedParams, + D9MethodQualifiedAsBytes, + D9MethodQualifiedAsBytesParams, + D9MethodToBeBytes, + D9MethodToBeBytesParams, + D9MethodToLeBytes, + D9MethodToLeBytesParams, + D9Mixed, + D9MixedParams, + D9MultiAssocConst, + D9MultiAssocConstParams, + D9NestedArrayField, + D9NestedArrayFieldParams, + D9NestedBytes, + D9NestedBytesParams, + D9NestedCombined, + D9NestedCombinedParams, + D9NestedDouble, + D9NestedDoubleParams, + // Nested seeds tests + D9NestedSimple, + D9NestedSimpleParams, + D9Param, + D9ParamBytes, + D9ParamBytesParams, + D9ParamParams, + // Qualified paths tests + D9QualifiedBare, + D9QualifiedBareParams, + D9QualifiedConstFn, + D9QualifiedConstFnParams, + D9QualifiedCrate, + D9QualifiedCrateParams, + D9QualifiedDeep, + D9QualifiedDeepParams, + D9QualifiedMixed, + D9QualifiedMixedParams, + D9QualifiedSelf, + D9QualifiedSelfParams, + D9Static, + D9StaticParams, + D9TraitAssocConst, + D9TraitAssocConstParams, }, instruction_accounts::{ - CreateFourMints, CreateFourMintsParams, CreatePdasAndMintAuto, CreateTwoMints, - CreateTwoMintsParams, + CreateMintWithMetadata, CreateMintWithMetadataParams, CreatePdasAndMintAuto, + CreateThreeMints, CreateThreeMintsParams, CreateTwoMints, CreateTwoMintsParams, }, FullAutoWithMintParams, LIGHT_CPI_SIGNER, }; @@ -215,18 +350,30 @@ pub mod csdk_anchor_full_derived_test { Ok(()) } - /// Test instruction that creates 4 mints in a single transaction. + /// Test instruction that creates 3 mints in a single transaction. /// Tests the multi-mint support in the RentFree macro scales beyond 2. #[allow(unused_variables)] - pub fn create_four_mints<'info>( - ctx: Context<'_, '_, '_, 'info, CreateFourMints<'info>>, - params: CreateFourMintsParams, + pub fn create_three_mints<'info>( + ctx: Context<'_, '_, '_, 'info, CreateThreeMints<'info>>, + params: CreateThreeMintsParams, ) -> Result<()> { - // All 4 mints are created by the RentFree macro in pre_init + // All 3 mints are created by the RentFree macro in pre_init // Nothing to do here - just verify all mints exist Ok(()) } + /// Test instruction that creates a mint with metadata. + /// Tests the metadata support in the RentFree macro. + #[allow(unused_variables)] + pub fn create_mint_with_metadata<'info>( + ctx: Context<'_, '_, '_, 'info, CreateMintWithMetadata<'info>>, + params: CreateMintWithMetadataParams, + ) -> Result<()> { + // Mint with metadata is created by the RentFree macro in pre_init + // Nothing to do here - metadata is part of the mint creation + Ok(()) + } + /// AMM initialize instruction with all rentfree markers. /// Tests: 2x #[rentfree], 2x #[rentfree_token], 1x #[light_mint], /// CreateTokenAccountCpi.rent_free(), CreateTokenAtaCpi.rent_free(), MintToCpi @@ -484,6 +631,542 @@ pub mod csdk_anchor_full_derived_test { Ok(()) } + // ========================================================================= + // D9 Qualified Paths Tests + // ========================================================================= + + /// D9: Bare constant (no path prefix) + pub fn d9_qualified_bare<'info>( + ctx: Context<'_, '_, '_, 'info, D9QualifiedBare<'info>>, + _params: D9QualifiedBareParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: self:: prefix path + pub fn d9_qualified_self<'info>( + ctx: Context<'_, '_, '_, 'info, D9QualifiedSelf<'info>>, + _params: D9QualifiedSelfParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: crate:: prefix path + pub fn d9_qualified_crate<'info>( + ctx: Context<'_, '_, '_, 'info, D9QualifiedCrate<'info>>, + _params: D9QualifiedCrateParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Deep nested crate path + pub fn d9_qualified_deep<'info>( + ctx: Context<'_, '_, '_, 'info, D9QualifiedDeep<'info>>, + _params: D9QualifiedDeepParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Mixed qualified and bare paths + pub fn d9_qualified_mixed<'info>( + ctx: Context<'_, '_, '_, 'info, D9QualifiedMixed<'info>>, + params: D9QualifiedMixedParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + // ========================================================================= + // D9 Method Chains Tests + // ========================================================================= + + /// D9: constant.as_ref() + pub fn d9_method_as_ref<'info>( + ctx: Context<'_, '_, '_, 'info, D9MethodAsRef<'info>>, + _params: D9MethodAsRefParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: string_constant.as_bytes() + pub fn d9_method_as_bytes<'info>( + ctx: Context<'_, '_, '_, 'info, D9MethodAsBytes<'info>>, + _params: D9MethodAsBytesParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: crate::path::CONST.as_bytes() + pub fn d9_method_qualified_as_bytes<'info>( + ctx: Context<'_, '_, '_, 'info, D9MethodQualifiedAsBytes<'info>>, + _params: D9MethodQualifiedAsBytesParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: params.field.to_le_bytes().as_ref() + pub fn d9_method_to_le_bytes<'info>( + ctx: Context<'_, '_, '_, 'info, D9MethodToLeBytes<'info>>, + _params: D9MethodToLeBytesParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: params.field.to_be_bytes().as_ref() + pub fn d9_method_to_be_bytes<'info>( + ctx: Context<'_, '_, '_, 'info, D9MethodToBeBytes<'info>>, + _params: D9MethodToBeBytesParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Mixed methods in seeds + pub fn d9_method_mixed<'info>( + ctx: Context<'_, '_, '_, 'info, D9MethodMixed<'info>>, + params: D9MethodMixedParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + // ========================================================================= + // D9 Array Bumps Tests (seed combinations with bump) + // ========================================================================= + + /// D9: Literal seed with bump + pub fn d9_bump_literal<'info>( + ctx: Context<'_, '_, '_, 'info, D9BumpLiteral<'info>>, + _params: D9BumpLiteralParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Constant seed with bump + pub fn d9_bump_constant<'info>( + ctx: Context<'_, '_, '_, 'info, D9BumpConstant<'info>>, + _params: D9BumpConstantParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Qualified path with bump + pub fn d9_bump_qualified<'info>( + ctx: Context<'_, '_, '_, 'info, D9BumpQualified<'info>>, + _params: D9BumpQualifiedParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Param seed with bump + pub fn d9_bump_param<'info>( + ctx: Context<'_, '_, '_, 'info, D9BumpParam<'info>>, + params: D9BumpParamParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: Ctx account seed with bump + pub fn d9_bump_ctx<'info>( + ctx: Context<'_, '_, '_, 'info, D9BumpCtx<'info>>, + _params: D9BumpCtxParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.authority.key(); + Ok(()) + } + + /// D9: Multiple seeds with bump + pub fn d9_bump_mixed<'info>( + ctx: Context<'_, '_, '_, 'info, D9BumpMixed<'info>>, + params: D9BumpMixedParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + // ========================================================================= + // D9 Complex Mixed Tests + // ========================================================================= + + /// D9: Three seeds + pub fn d9_complex_three<'info>( + ctx: Context<'_, '_, '_, 'info, D9ComplexThree<'info>>, + params: D9ComplexThreeParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: Four seeds + pub fn d9_complex_four<'info>( + ctx: Context<'_, '_, '_, 'info, D9ComplexFour<'info>>, + params: D9ComplexFourParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: Five seeds with ctx account + pub fn d9_complex_five<'info>( + ctx: Context<'_, '_, '_, 'info, D9ComplexFive<'info>>, + params: D9ComplexFiveParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: Qualified paths mixed with local + pub fn d9_complex_qualified_mix<'info>( + ctx: Context<'_, '_, '_, 'info, D9ComplexQualifiedMix<'info>>, + params: D9ComplexQualifiedMixParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: Function call combined with other seeds + pub fn d9_complex_func<'info>( + ctx: Context<'_, '_, '_, 'info, D9ComplexFunc<'info>>, + params: D9ComplexFuncParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.key_a; + Ok(()) + } + + /// D9: All qualified paths + pub fn d9_complex_all_qualified<'info>( + ctx: Context<'_, '_, '_, 'info, D9ComplexAllQualified<'info>>, + params: D9ComplexAllQualifiedParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: Program ID as seed + pub fn d9_complex_program_id<'info>( + ctx: Context<'_, '_, '_, 'info, D9ComplexProgramId<'info>>, + params: D9ComplexProgramIdParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: id() function call as seed + pub fn d9_complex_id_func<'info>( + ctx: Context<'_, '_, '_, 'info, D9ComplexIdFunc<'info>>, + params: D9ComplexIdFuncParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + // ========================================================================= + // D9 Edge Cases Tests + // ========================================================================= + + /// D9: Empty literal + pub fn d9_edge_empty<'info>( + ctx: Context<'_, '_, '_, 'info, D9EdgeEmpty<'info>>, + params: D9EdgeEmptyParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: Single byte constant + pub fn d9_edge_single_byte<'info>( + ctx: Context<'_, '_, '_, 'info, D9EdgeSingleByte<'info>>, + _params: D9EdgeSingleByteParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Single letter constant name + pub fn d9_edge_single_letter<'info>( + ctx: Context<'_, '_, '_, 'info, D9EdgeSingleLetter<'info>>, + _params: D9EdgeSingleLetterParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Constant name with digits + pub fn d9_edge_digits<'info>( + ctx: Context<'_, '_, '_, 'info, D9EdgeDigits<'info>>, + _params: D9EdgeDigitsParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Leading underscore constant + pub fn d9_edge_underscore<'info>( + ctx: Context<'_, '_, '_, 'info, D9EdgeUnderscore<'info>>, + _params: D9EdgeUnderscoreParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Many literals in seeds + pub fn d9_edge_many_literals<'info>( + ctx: Context<'_, '_, '_, 'info, D9EdgeManyLiterals<'info>>, + _params: D9EdgeManyLiteralsParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Mixed edge cases + pub fn d9_edge_mixed<'info>( + ctx: Context<'_, '_, '_, 'info, D9EdgeMixed<'info>>, + params: D9EdgeMixedParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + // ========================================================================= + // D9 External Paths Tests + // ========================================================================= + + /// D9: External crate (light_sdk_types) + pub fn d9_external_sdk_types<'info>( + ctx: Context<'_, '_, '_, 'info, D9ExternalSdkTypes<'info>>, + params: D9ExternalSdkTypesParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: External crate (light_ctoken_types) + pub fn d9_external_ctoken<'info>( + ctx: Context<'_, '_, '_, 'info, D9ExternalCtoken<'info>>, + params: D9ExternalCtokenParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: Multiple external crates mixed + pub fn d9_external_mixed<'info>( + ctx: Context<'_, '_, '_, 'info, D9ExternalMixed<'info>>, + params: D9ExternalMixedParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: External with local constant + pub fn d9_external_with_local<'info>( + ctx: Context<'_, '_, '_, 'info, D9ExternalWithLocal<'info>>, + params: D9ExternalWithLocalParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: External constant with bump + pub fn d9_external_bump<'info>( + ctx: Context<'_, '_, '_, 'info, D9ExternalBump<'info>>, + params: D9ExternalBumpParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: Re-exported external constant + pub fn d9_external_reexport<'info>( + ctx: Context<'_, '_, '_, 'info, D9ExternalReexport<'info>>, + _params: D9ExternalReexportParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + // ========================================================================= + // D9 Nested Seeds Tests + // ========================================================================= + + /// D9: Simple nested struct access + pub fn d9_nested_simple<'info>( + ctx: Context<'_, '_, '_, 'info, D9NestedSimple<'info>>, + params: D9NestedSimpleParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.nested.owner; + Ok(()) + } + + /// D9: Double nested struct access + pub fn d9_nested_double<'info>( + ctx: Context<'_, '_, '_, 'info, D9NestedDouble<'info>>, + params: D9NestedDoubleParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.outer.nested.owner; + Ok(()) + } + + /// D9: Nested array field access + pub fn d9_nested_array_field<'info>( + ctx: Context<'_, '_, '_, 'info, D9NestedArrayField<'info>>, + params: D9NestedArrayFieldParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.outer.nested.owner; + Ok(()) + } + + /// D9: Array indexing params.arrays[2].as_slice() + pub fn d9_array_index<'info>( + ctx: Context<'_, '_, '_, 'info, D9ArrayIndex<'info>>, + _params: D9ArrayIndexParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Nested struct with bytes conversion + pub fn d9_nested_bytes<'info>( + ctx: Context<'_, '_, '_, 'info, D9NestedBytes<'info>>, + params: D9NestedBytesParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.nested.owner; + Ok(()) + } + + /// D9: Multiple nested seeds combined + pub fn d9_nested_combined<'info>( + ctx: Context<'_, '_, '_, 'info, D9NestedCombined<'info>>, + params: D9NestedCombinedParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.outer.nested.owner; + Ok(()) + } + + // ========================================================================= + // D9 Const Patterns Tests + // ========================================================================= + + /// D9: Associated constant + pub fn d9_assoc_const<'info>( + ctx: Context<'_, '_, '_, 'info, D9AssocConst<'info>>, + _params: D9AssocConstParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Associated constant with method + pub fn d9_assoc_const_method<'info>( + ctx: Context<'_, '_, '_, 'info, D9AssocConstMethod<'info>>, + _params: D9AssocConstMethodParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Multiple associated constants + pub fn d9_multi_assoc_const<'info>( + ctx: Context<'_, '_, '_, 'info, D9MultiAssocConst<'info>>, + params: D9MultiAssocConstParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + + /// D9: Const fn call + pub fn d9_const_fn<'info>( + ctx: Context<'_, '_, '_, 'info, D9ConstFn<'info>>, + _params: D9ConstFnParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Const fn with generic + pub fn d9_const_fn_generic<'info>( + ctx: Context<'_, '_, '_, 'info, D9ConstFnGeneric<'info>>, + _params: D9ConstFnGenericParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Trait associated constant + pub fn d9_trait_assoc_const<'info>( + ctx: Context<'_, '_, '_, 'info, D9TraitAssocConst<'info>>, + _params: D9TraitAssocConstParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Static variable + pub fn d9_static<'info>( + ctx: Context<'_, '_, '_, 'info, D9Static<'info>>, + _params: D9StaticParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Qualified const fn + pub fn d9_qualified_const_fn<'info>( + ctx: Context<'_, '_, '_, 'info, D9QualifiedConstFn<'info>>, + _params: D9QualifiedConstFnParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Fully qualified associated constant + pub fn d9_fully_qualified_assoc<'info>( + ctx: Context<'_, '_, '_, 'info, D9FullyQualifiedAssoc<'info>>, + _params: D9FullyQualifiedAssocParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Fully qualified trait associated constant + pub fn d9_fully_qualified_trait<'info>( + ctx: Context<'_, '_, '_, 'info, D9FullyQualifiedTrait<'info>>, + _params: D9FullyQualifiedTraitParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Fully qualified const fn with generic + pub fn d9_fully_qualified_generic<'info>( + ctx: Context<'_, '_, '_, 'info, D9FullyQualifiedGeneric<'info>>, + _params: D9FullyQualifiedGenericParams, + ) -> Result<()> { + ctx.accounts.record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Combined const patterns + pub fn d9_const_combined<'info>( + ctx: Context<'_, '_, '_, 'info, D9ConstCombined<'info>>, + params: D9ConstCombinedParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } + // ========================================================================= // D5 Additional Markers Tests // ========================================================================= diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs index fa7288df1c..a6cf5edf87 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs @@ -273,7 +273,7 @@ fn test_pack_converts_pool_id_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = observation_state.pack(&mut packed_accounts); + let packed = observation_state.pack(&mut packed_accounts).unwrap(); // The pool_id should have been added to packed_accounts and assigned index 0 assert_eq!(packed.pool_id, 0u8); @@ -311,7 +311,7 @@ fn test_pack_with_pre_existing_pubkeys() { // Pre-insert another pubkey packed_accounts.insert_or_get(Pubkey::new_unique()); - let packed = observation_state.pack(&mut packed_accounts); + let packed = observation_state.pack(&mut packed_accounts).unwrap(); // The pool_id should have been added and assigned index 1 (since index 0 is taken) assert_eq!(packed.pool_id, 1u8); @@ -342,7 +342,7 @@ fn test_pack_preserves_all_fields() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = observation_state.pack(&mut packed_accounts); + let packed = observation_state.pack(&mut packed_accounts).unwrap(); assert!(packed.initialized); assert_eq!(packed.observation_index, 42); @@ -378,7 +378,7 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = observation_with_info.pack(&mut packed_accounts); + let packed = observation_with_info.pack(&mut packed_accounts).unwrap(); assert!( packed.compression_info.is_none(), @@ -432,8 +432,8 @@ fn test_pack_different_pool_ids_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = observation1.pack(&mut packed_accounts); - let packed2 = observation2.pack(&mut packed_accounts); + let packed1 = observation1.pack(&mut packed_accounts).unwrap(); + let packed2 = observation2.pack(&mut packed_accounts).unwrap(); // Different pool IDs should get different indices assert_ne!( @@ -487,8 +487,8 @@ fn test_pack_reuses_same_pool_id_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = observation1.pack(&mut packed_accounts); - let packed2 = observation2.pack(&mut packed_accounts); + let packed1 = observation1.pack(&mut packed_accounts).unwrap(); + let packed2 = observation2.pack(&mut packed_accounts).unwrap(); // Same pool_id should get same index assert_eq!( @@ -522,7 +522,7 @@ fn test_pack_stores_pool_id_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = observation_state.pack(&mut packed_accounts); + let packed = observation_state.pack(&mut packed_accounts).unwrap(); let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 1, "should have 1 pubkey stored"); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs index b0cdbd986b..4d71974558 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs @@ -295,7 +295,7 @@ fn test_pack_converts_all_10_pubkeys_to_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = pool.pack(&mut packed_accounts); + let packed = pool.pack(&mut packed_accounts).unwrap(); // All 10 pubkeys should have been added and assigned indices 0-9 assert_eq!(packed.amm_config, 0u8); @@ -349,7 +349,7 @@ fn test_pack_reuses_same_pubkey_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = pool.pack(&mut packed_accounts); + let packed = pool.pack(&mut packed_accounts).unwrap(); // Same pubkey should get same index assert_eq!( @@ -388,7 +388,7 @@ fn test_pack_preserves_numeric_fields() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = pool.pack(&mut packed_accounts); + let packed = pool.pack(&mut packed_accounts).unwrap(); assert_eq!(packed.auth_bump, 127); assert_eq!(packed.status, 2); @@ -435,7 +435,7 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = pool_with_info.pack(&mut packed_accounts); + let packed = pool_with_info.pack(&mut packed_accounts).unwrap(); assert!( packed.compression_info.is_none(), @@ -500,8 +500,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = pool1.pack(&mut packed_accounts); - let packed2 = pool2.pack(&mut packed_accounts); + let packed1 = pool1.pack(&mut packed_accounts).unwrap(); + let packed2 = pool2.pack(&mut packed_accounts).unwrap(); // Different pubkeys should get different indices assert_ne!( @@ -553,7 +553,7 @@ fn test_pack_stores_all_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let _packed = pool.pack(&mut packed_accounts); + let _packed = pool.pack(&mut packed_accounts).unwrap(); let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 10, "should have 10 pubkeys stored"); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs index 064ffd25de..51ba3613c6 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs @@ -325,7 +325,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The player should have been added to packed_accounts // and packed.player should be the index (0 for first pubkey) @@ -334,7 +334,7 @@ fn test_pack_converts_pubkey_to_index() { let mut packed_accounts = PackedAccounts::default(); packed_accounts.insert_or_get(Pubkey::new_unique()); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The player should have been added to packed_accounts // and packed.player should be the index (1 for second pubkey) @@ -367,8 +367,8 @@ fn test_pack_reuses_same_pubkey_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Same pubkey should get same index assert_eq!( @@ -400,8 +400,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Different pubkeys should get different indices assert_ne!( @@ -433,8 +433,8 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record_with_info.pack(&mut packed_accounts); - let packed2 = record_without_info.pack(&mut packed_accounts); + let packed1 = record_with_info.pack(&mut packed_accounts).unwrap(); + let packed2 = record_without_info.pack(&mut packed_accounts).unwrap(); // Both packed structs should have compression_info = None assert!( @@ -473,8 +473,8 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Verify pubkeys are stored and retrievable let stored_pubkeys = packed_accounts.packed_pubkeys(); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs index e0ea23b2bc..ce29bf8a2e 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs @@ -248,7 +248,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The owner should have been added to packed_accounts // and packed.owner should be the index (0 for first pubkey) @@ -258,7 +258,7 @@ fn test_pack_converts_pubkey_to_index() { let mut packed_accounts = PackedAccounts::default(); packed_accounts.insert_or_get(Pubkey::new_unique()); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The owner should have been added to packed_accounts // and packed.owner should be the index (1 for second pubkey) @@ -288,8 +288,8 @@ fn test_pack_reuses_same_pubkey_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Same pubkey should get same index assert_eq!( @@ -317,8 +317,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Different pubkeys should get different indices assert_ne!( @@ -346,8 +346,8 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record_with_info.pack(&mut packed_accounts); - let packed2 = record_without_info.pack(&mut packed_accounts); + let packed1 = record_with_info.pack(&mut packed_accounts).unwrap(); + let packed2 = record_without_info.pack(&mut packed_accounts).unwrap(); // Both packed structs should have compression_info = None assert!( @@ -382,8 +382,8 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Verify pubkeys are stored and retrievable let stored_pubkeys = packed_accounts.packed_pubkeys(); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs index 93eb0bbadd..40047cca64 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs @@ -248,7 +248,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The owner should have been added to packed_accounts // and packed.owner should be the index (0 for first pubkey) @@ -258,7 +258,7 @@ fn test_pack_converts_pubkey_to_index() { let mut packed_accounts = PackedAccounts::default(); packed_accounts.insert_or_get(Pubkey::new_unique()); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The owner should have been added to packed_accounts // and packed.owner should be the index (1 for second pubkey) @@ -288,8 +288,8 @@ fn test_pack_reuses_same_pubkey_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Same pubkey should get same index assert_eq!( @@ -317,8 +317,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Different pubkeys should get different indices assert_ne!( @@ -346,8 +346,8 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record_with_info.pack(&mut packed_accounts); - let packed2 = record_without_info.pack(&mut packed_accounts); + let packed1 = record_with_info.pack(&mut packed_accounts).unwrap(); + let packed2 = record_without_info.pack(&mut packed_accounts).unwrap(); // Both packed structs should have compression_info = None assert!( @@ -382,8 +382,8 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Verify pubkeys are stored and retrievable let stored_pubkeys = packed_accounts.packed_pubkeys(); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs index 5d71081a7f..4217eac0c1 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs @@ -455,7 +455,7 @@ fn test_pack_converts_all_pubkey_types() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // Direct Pubkey fields are converted to u8 indices assert_eq!(packed.owner, 0u8); @@ -496,7 +496,7 @@ fn test_pack_with_option_pubkey_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // Only three pubkeys should have been added assert_eq!(packed.owner, 0u8); @@ -546,8 +546,8 @@ fn test_pack_reuses_pubkey_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Same pubkeys should get same indices assert_eq!(packed1.owner, packed2.owner); @@ -584,7 +584,7 @@ fn test_pack_preserves_non_pubkey_fields() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // All non-Pubkey fields should be preserved assert_eq!(packed.name, name); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs index 7019ee783c..a24130bbd5 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs @@ -261,7 +261,7 @@ fn test_pack_converts_all_pubkeys_to_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // All three Pubkeys should have been added and packed should have their indices assert_eq!(packed.owner, 0u8); @@ -299,8 +299,8 @@ fn test_pack_reuses_pubkey_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Same pubkeys should get same indices assert_eq!(packed1.owner, packed2.owner); @@ -331,8 +331,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Different pubkeys should get different indices assert_ne!( @@ -368,8 +368,8 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record_with_info.pack(&mut packed_accounts); - let packed2 = record_without_info.pack(&mut packed_accounts); + let packed1 = record_with_info.pack(&mut packed_accounts).unwrap(); + let packed2 = record_without_info.pack(&mut packed_accounts).unwrap(); // Both packed structs should have compression_info = None assert!( @@ -409,8 +409,8 @@ fn test_pack_stores_all_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Verify pubkeys are stored and retrievable let stored_pubkeys = packed_accounts.packed_pubkeys(); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs index 86fb31a17f..12bc2b69d1 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs @@ -240,7 +240,7 @@ fn test_pack_converts_pubkey_fields_to_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The packed struct should have owner as u8 index (0 since it's first pubkey) assert_eq!(packed.owner, 0u8); @@ -261,7 +261,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The owner should have been added and packed.owner should be the index (0 for first pubkey) assert_eq!(packed.owner, 0u8); @@ -290,7 +290,7 @@ fn test_pack_preserves_option_pubkey_as_option_pubkey() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // Direct Pubkey field is converted to u8 index assert_eq!(packed.owner, 0u8); @@ -319,7 +319,7 @@ fn test_pack_option_pubkey_none_stays_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // Direct Pubkey field is converted to u8 index assert_eq!(packed.owner, 0u8); @@ -353,7 +353,7 @@ fn test_pack_all_option_pubkeys_some() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // Direct Pubkey field is converted to u8 index assert_eq!(packed.owner, 0u8); @@ -380,7 +380,7 @@ fn test_pack_all_option_pubkeys_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // Only owner should have been added assert_eq!(packed.owner, 0u8); @@ -415,8 +415,8 @@ fn test_pack_reuses_same_pubkey_index_for_direct_fields() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Same direct Pubkey field should get same index assert_eq!( diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs index 468a184aff..5d4e749b9e 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs @@ -164,7 +164,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The owner should have been added to packed_accounts // and packed.owner should be the index (0 for first pubkey) @@ -173,7 +173,7 @@ fn test_pack_converts_pubkey_to_index() { let mut packed_accounts = PackedAccounts::default(); packed_accounts.insert_or_get(Pubkey::new_unique()); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The owner should have been added to packed_accounts // and packed.owner should be the index (0 for second pubkey) @@ -198,8 +198,8 @@ fn test_pack_reuses_same_pubkey_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Same pubkey should get same index assert_eq!( @@ -223,8 +223,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Different pubkeys should get different indices assert_ne!( @@ -252,8 +252,8 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record_with_info.pack(&mut packed_accounts); - let packed2 = record_without_info.pack(&mut packed_accounts); + let packed1 = record_with_info.pack(&mut packed_accounts).unwrap(); + let packed2 = record_without_info.pack(&mut packed_accounts).unwrap(); // Both packed structs should have compression_info = None assert!( @@ -284,8 +284,8 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Verify pubkeys are stored and retrievable let stored_pubkeys = packed_accounts.packed_pubkeys(); @@ -314,7 +314,7 @@ fn test_pack_index_assignment_order() { owner: *owner, counter: 0, }; - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); indices.push(packed.owner); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs index ebf4f35d15..d4ed5ee2ab 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs @@ -348,7 +348,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); assert_eq!(packed.owner, 0u8); assert_eq!(packed.time, 50); @@ -386,8 +386,8 @@ fn test_pack_reuses_same_pubkey_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); assert_eq!( packed1.owner, packed2.owner, @@ -420,8 +420,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); assert_ne!( packed1.owner, packed2.owner, @@ -454,8 +454,8 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record_with_info.pack(&mut packed_accounts); - let packed2 = record_without_info.pack(&mut packed_accounts); + let packed1 = record_with_info.pack(&mut packed_accounts).unwrap(); + let packed2 = record_without_info.pack(&mut packed_accounts).unwrap(); assert!( packed1.compression_info.is_none(), @@ -495,8 +495,8 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); @@ -528,7 +528,7 @@ fn test_pack_index_assignment_order() { counter: 0, flag: false, }; - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); indices.push(packed.owner); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs index df1d5c07a1..5c47716985 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs @@ -286,7 +286,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); assert_eq!(packed.owner, 0u8); assert_eq!(packed.start, 50); @@ -318,8 +318,8 @@ fn test_pack_reuses_same_pubkey_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); assert_eq!( packed1.owner, packed2.owner, @@ -348,8 +348,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); assert_ne!( packed1.owner, packed2.owner, @@ -378,8 +378,8 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record_with_info.pack(&mut packed_accounts); - let packed2 = record_without_info.pack(&mut packed_accounts); + let packed1 = record_with_info.pack(&mut packed_accounts).unwrap(); + let packed2 = record_without_info.pack(&mut packed_accounts).unwrap(); assert!( packed1.compression_info.is_none(), @@ -415,8 +415,8 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); @@ -446,7 +446,7 @@ fn test_pack_index_assignment_order() { cached: 0, counter: 0, }; - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); indices.push(packed.owner); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs index dbe1f6cf89..b82e0457ad 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs @@ -222,7 +222,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); assert_eq!(packed.owner, 0u8); assert_eq!(packed.counter, 100); @@ -248,8 +248,8 @@ fn test_pack_reuses_same_pubkey_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); assert_eq!( packed1.owner, packed2.owner, @@ -274,8 +274,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); assert_ne!( packed1.owner, packed2.owner, @@ -300,8 +300,8 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record_with_info.pack(&mut packed_accounts); - let packed2 = record_without_info.pack(&mut packed_accounts); + let packed1 = record_with_info.pack(&mut packed_accounts).unwrap(); + let packed2 = record_without_info.pack(&mut packed_accounts).unwrap(); assert!( packed1.compression_info.is_none(), @@ -333,8 +333,8 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); @@ -362,7 +362,7 @@ fn test_pack_index_assignment_order() { counter: 0, flag: false, }; - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); indices.push(packed.owner); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs index 84e0ce5131..6bc5e3553a 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs @@ -298,7 +298,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); assert_eq!(packed.owner, 0u8); assert_eq!(packed.start_time, 50); @@ -327,8 +327,8 @@ fn test_pack_reuses_same_pubkey_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); assert_eq!( packed1.owner, packed2.owner, @@ -355,8 +355,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); assert_ne!( packed1.owner, packed2.owner, @@ -383,8 +383,8 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record_with_info.pack(&mut packed_accounts); - let packed2 = record_without_info.pack(&mut packed_accounts); + let packed1 = record_with_info.pack(&mut packed_accounts).unwrap(); + let packed2 = record_without_info.pack(&mut packed_accounts).unwrap(); assert!( packed1.compression_info.is_none(), @@ -418,8 +418,8 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); @@ -448,7 +448,7 @@ fn test_pack_index_assignment_order() { end_time: None, counter: 0, }; - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); indices.push(packed.owner); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs index 4a5246d45b..9deb78210c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs @@ -221,7 +221,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); assert_eq!(packed.owner, 0u8); assert_eq!(packed.cached, 50); @@ -247,8 +247,8 @@ fn test_pack_reuses_same_pubkey_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); assert_eq!( packed1.owner, packed2.owner, @@ -273,8 +273,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); assert_ne!( packed1.owner, packed2.owner, @@ -299,8 +299,8 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record_with_info.pack(&mut packed_accounts); - let packed2 = record_without_info.pack(&mut packed_accounts); + let packed1 = record_with_info.pack(&mut packed_accounts).unwrap(); + let packed2 = record_without_info.pack(&mut packed_accounts).unwrap(); assert!( packed1.compression_info.is_none(), @@ -332,8 +332,8 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); @@ -361,7 +361,7 @@ fn test_pack_index_assignment_order() { cached: 0, counter: 0, }; - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); indices.push(packed.owner); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs index cbc11bfc64..ff2055d4d2 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs @@ -364,7 +364,7 @@ fn test_pack_converts_all_pubkeys_to_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // Direct Pubkey fields are converted to u8 indices assert_eq!(packed.owner, 0u8); // First pubkey @@ -400,7 +400,7 @@ fn test_pack_does_not_apply_compress_as_overrides() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // Pack preserves field values - compress_as overrides are NOT applied assert_eq!(packed.cached_time, 999, "pack preserves cached_time value"); @@ -436,7 +436,7 @@ fn test_compress_as_then_pack_applies_overrides() { // Chain compress_as() then pack() let compressed = record.compress_as(); let mut packed_accounts = PackedAccounts::default(); - let packed = compressed.pack(&mut packed_accounts); + let packed = compressed.pack(&mut packed_accounts).unwrap(); // compress_as overrides ARE applied when chained assert_eq!( @@ -476,7 +476,7 @@ fn test_pack_preserves_start_time_without_override() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); assert_eq!( packed.start_time, start_time_value, @@ -509,7 +509,7 @@ fn test_pack_reuses_duplicate_pubkeys_for_direct_fields() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record1.pack(&mut packed_accounts); + let packed = record1.pack(&mut packed_accounts).unwrap(); // owner and delegate are the same pubkey, should get the same index assert_eq!( @@ -547,7 +547,7 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); assert!( packed.compression_info.is_none(), diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs index a2a023e49d..d54234f11e 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs @@ -204,7 +204,7 @@ fn test_pack_converts_pubkey_to_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The owner should have been added to packed_accounts // and packed.owner should be the index (0 for first pubkey) @@ -214,7 +214,7 @@ fn test_pack_converts_pubkey_to_index() { let mut packed_accounts = PackedAccounts::default(); packed_accounts.insert_or_get(Pubkey::new_unique()); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // The owner should have been added to packed_accounts // and packed.owner should be the index (1 for second pubkey) @@ -241,8 +241,8 @@ fn test_pack_reuses_same_pubkey_index() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Same pubkey should get same index assert_eq!( @@ -265,7 +265,7 @@ fn test_pack_preserves_counter_and_flag() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record.pack(&mut packed_accounts); + let packed = record.pack(&mut packed_accounts).unwrap(); // counter and flag should be preserved assert_eq!(packed.counter, counter); @@ -284,7 +284,7 @@ fn test_pack_sets_compression_info_to_none() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = record_with_info.pack(&mut packed_accounts); + let packed = record_with_info.pack(&mut packed_accounts).unwrap(); assert!( packed.compression_info.is_none(), @@ -309,8 +309,8 @@ fn test_pack_different_pubkeys_get_different_indices() { }; let mut packed_accounts = PackedAccounts::default(); - let packed1 = record1.pack(&mut packed_accounts); - let packed2 = record2.pack(&mut packed_accounts); + let packed1 = record1.pack(&mut packed_accounts).unwrap(); + let packed2 = record2.pack(&mut packed_accounts).unwrap(); // Different pubkeys should get different indices assert_ne!( diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs index 3a5cdf6755..d851ffe820 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs @@ -79,7 +79,9 @@ pub fn assert_compression_info_returns_reference< T: HasCompressionInfo + CompressibleTestFactory, >() { let record = T::with_compression_info(); - let info = record.compression_info(); + let info = record + .compression_info() + .expect("compression_info should be Some"); // Just verify we can access it - the default values assert_eq!(info.config_version, 0); assert_eq!(info.lamports_per_write, 0); @@ -92,13 +94,27 @@ pub fn assert_compression_info_mut_allows_modification< let mut record = T::with_compression_info(); { - let info = record.compression_info_mut(); + let info = record + .compression_info_mut() + .expect("compression_info should be Some"); info.config_version = 99; info.lamports_per_write = 1000; } - assert_eq!(record.compression_info().config_version, 99); - assert_eq!(record.compression_info().lamports_per_write, 1000); + assert_eq!( + record + .compression_info() + .expect("compression_info should be Some") + .config_version, + 99 + ); + assert_eq!( + record + .compression_info() + .expect("compression_info should be Some") + .lamports_per_write, + 1000 + ); } /// Verifies compression_info_mut_opt() returns a mutable reference to the Option. @@ -128,28 +144,30 @@ pub fn assert_set_compression_info_none_works() { +/// Verifies compression_info() returns Err when compression_info is None. +pub fn assert_compression_info_returns_err_when_none< + T: HasCompressionInfo + CompressibleTestFactory, +>() { let record = T::without_compression_info(); - // This should panic since compression_info is None - let _ = record.compression_info(); + // This should return Err since compression_info is None + assert!(record.compression_info().is_err()); } -/// Verifies compression_info_mut() panics when compression_info is None. -/// Call this from a test marked with `#[should_panic]`. -pub fn assert_compression_info_mut_panics_when_none< +/// Verifies compression_info_mut() returns Err when compression_info is None. +pub fn assert_compression_info_mut_returns_err_when_none< T: HasCompressionInfo + CompressibleTestFactory, >() { let mut record = T::without_compression_info(); - // This should panic since compression_info is None - let _ = record.compression_info_mut(); + // This should return Err since compression_info is None + assert!(record.compression_info_mut().is_err()); } // ============================================================================= @@ -191,7 +209,7 @@ pub fn assert_compress_as_returns_owned_cow< /// Verifies size() returns a positive value. pub fn assert_size_returns_positive() { let record = T::with_compression_info(); - let size = record.size(); + let size = record.size().expect("size should succeed"); assert!(size > 0, "size should be positive"); } @@ -200,8 +218,8 @@ pub fn assert_size_is_deterministic() let record = T::with_compression_info(); let record_clone = record.clone(); - let size1 = record.size(); - let size2 = record_clone.size(); + let size1 = record.size().expect("size should succeed"); + let size2 = record_clone.size().expect("size should succeed"); assert_eq!(size1, size2, "size should be deterministic for same data"); } @@ -330,15 +348,13 @@ macro_rules! generate_trait_tests { } #[test] - #[should_panic] - fn test_compression_info_panics_when_none() { - assert_compression_info_panics_when_none::<$type>(); + fn test_compression_info_returns_err_when_none() { + assert_compression_info_returns_err_when_none::<$type>(); } #[test] - #[should_panic] - fn test_compression_info_mut_panics_when_none() { - assert_compression_info_mut_panics_when_none::<$type>(); + fn test_compression_info_mut_returns_err_when_none() { + assert_compression_info_mut_returns_err_when_none::<$type>(); } } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs index b5867c044c..5ea375fb04 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs @@ -663,13 +663,12 @@ async fn test_create_two_mints() { ); } -/// Test creating 4 mints in a single instruction. +/// Test creating multiple mints (3) in a single instruction. /// Verifies multi-mint support in the RentFree macro scales beyond 2. #[tokio::test] -async fn test_create_four_mints() { +async fn test_create_multi_mints() { use csdk_anchor_full_derived_test::instruction_accounts::{ - CreateFourMintsParams, MINT_SIGNER_A_SEED, MINT_SIGNER_B_SEED, MINT_SIGNER_C_SEED, - MINT_SIGNER_D_SEED, + CreateThreeMintsParams, MINT_SIGNER_A_SEED, MINT_SIGNER_B_SEED, MINT_SIGNER_C_SEED, }; use light_token_sdk::token::{ find_mint_address as find_cmint_address, COMPRESSIBLE_CONFIG_V1, @@ -703,7 +702,7 @@ async fn test_create_four_mints() { let authority = Keypair::new(); - // Derive PDAs for all 4 mint signers + // Derive PDAs for all 3 mint signers let (mint_signer_a_pda, mint_signer_a_bump) = Pubkey::find_program_address( &[MINT_SIGNER_A_SEED, authority.pubkey().as_ref()], &program_id, @@ -716,18 +715,13 @@ async fn test_create_four_mints() { &[MINT_SIGNER_C_SEED, authority.pubkey().as_ref()], &program_id, ); - let (mint_signer_d_pda, mint_signer_d_bump) = Pubkey::find_program_address( - &[MINT_SIGNER_D_SEED, authority.pubkey().as_ref()], - &program_id, - ); // Derive mint PDAs let (cmint_a_pda, _) = find_cmint_address(&mint_signer_a_pda); let (cmint_b_pda, _) = find_cmint_address(&mint_signer_b_pda); let (cmint_c_pda, _) = find_cmint_address(&mint_signer_c_pda); - let (cmint_d_pda, _) = find_cmint_address(&mint_signer_d_pda); - // Get proof for all 4 mints + // Get proof for all 3 mints let proof_result = get_create_accounts_proof( &rpc, &program_id, @@ -735,23 +729,20 @@ async fn test_create_four_mints() { CreateAccountsProofInput::mint(mint_signer_a_pda), CreateAccountsProofInput::mint(mint_signer_b_pda), CreateAccountsProofInput::mint(mint_signer_c_pda), - CreateAccountsProofInput::mint(mint_signer_d_pda), ], ) .await .unwrap(); - let accounts = csdk_anchor_full_derived_test::accounts::CreateFourMints { + let accounts = csdk_anchor_full_derived_test::accounts::CreateThreeMints { fee_payer: payer.pubkey(), authority: authority.pubkey(), mint_signer_a: mint_signer_a_pda, mint_signer_b: mint_signer_b_pda, mint_signer_c: mint_signer_c_pda, - mint_signer_d: mint_signer_d_pda, cmint_a: cmint_a_pda, cmint_b: cmint_b_pda, cmint_c: cmint_c_pda, - cmint_d: cmint_d_pda, compression_config: config_pda, ctoken_compressible_config: COMPRESSIBLE_CONFIG_V1, ctoken_rent_sponsor: CTOKEN_RENT_SPONSOR, @@ -760,13 +751,12 @@ async fn test_create_four_mints() { system_program: solana_sdk::system_program::ID, }; - let instruction_data = csdk_anchor_full_derived_test::instruction::CreateFourMints { - params: CreateFourMintsParams { + let instruction_data = csdk_anchor_full_derived_test::instruction::CreateThreeMints { + params: CreateThreeMintsParams { create_accounts_proof: proof_result.create_accounts_proof, mint_signer_a_bump, mint_signer_b_bump, mint_signer_c_bump, - mint_signer_d_bump, }, }; @@ -782,9 +772,9 @@ async fn test_create_four_mints() { rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) .await - .expect("CreateFourMints should succeed"); + .expect("CreateThreeMints should succeed"); - // Verify all 4 mints exist on-chain + // Verify all 3 mints exist on-chain use light_token_interface::state::Mint; let cmint_a_account = rpc @@ -802,11 +792,6 @@ async fn test_create_four_mints() { .await .unwrap() .expect("Mint C should exist on-chain"); - let cmint_d_account = rpc - .get_account(cmint_d_pda) - .await - .unwrap() - .expect("Mint D should exist on-chain"); // Parse and verify mint data let mint_a: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_a_account.data[..]) @@ -815,14 +800,11 @@ async fn test_create_four_mints() { .expect("Failed to deserialize Mint B"); let mint_c: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_c_account.data[..]) .expect("Failed to deserialize Mint C"); - let mint_d: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_d_account.data[..]) - .expect("Failed to deserialize Mint D"); // Verify decimals match what was specified in #[light_mint] assert_eq!(mint_a.base.decimals, 6, "Mint A should have 6 decimals"); assert_eq!(mint_b.base.decimals, 8, "Mint B should have 8 decimals"); assert_eq!(mint_c.base.decimals, 9, "Mint C should have 9 decimals"); - assert_eq!(mint_d.base.decimals, 12, "Mint D should have 12 decimals"); // Verify mint authorities assert_eq!( @@ -840,9 +822,4 @@ async fn test_create_four_mints() { Some(payer.pubkey().to_bytes().into()), "Mint C authority should be fee_payer" ); - assert_eq!( - mint_d.base.mint_authority, - Some(payer.pubkey().to_bytes().into()), - "Mint D authority should be fee_payer" - ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs index 6ef5900474..e1b8014e21 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs @@ -3,6 +3,8 @@ //! These tests verify that the macro-generated code works correctly at runtime //! by testing the full lifecycle: create account -> verify on-chain -> compress -> decompress. +#![allow(clippy::useless_asref)] // Testing that macro handles .as_ref() patterns + mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; @@ -1770,3 +1772,3189 @@ async fn test_d7_all_names() { .await; // Note: Token vault decompression not tested - requires TokenAccountVariant } + +// ============================================================================= +// D9 Qualified Paths Tests +// ============================================================================= + +/// Tests D9QualifiedBare: Bare constant (no path prefix) +#[tokio::test] +async fn test_d9_qualified_bare() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9QualifiedBareParams, + instructions::d9_seeds::qualified_paths::D9_QUALIFIED_LOCAL, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA using bare constant + let (pda, _) = Pubkey::find_program_address(&[D9_QUALIFIED_LOCAL], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9QualifiedBare { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9QualifiedBare { + _params: D9QualifiedBareParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9QualifiedBare instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9QualifiedSelf: self:: prefix path qualification +#[tokio::test] +async fn test_d9_qualified_self() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9QualifiedSelfParams, + instructions::d9_seeds::qualified_paths::D9_QUALIFIED_LOCAL, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA using self:: prefix (same constant as bare) + let (pda, _) = Pubkey::find_program_address(&[D9_QUALIFIED_LOCAL], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9QualifiedSelf { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9QualifiedSelf { + _params: D9QualifiedSelfParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9QualifiedSelf instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9QualifiedCrate: crate:: prefix path qualification +#[tokio::test] +async fn test_d9_qualified_crate() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9QualifiedCrateParams, + instructions::d9_seeds::qualified_paths::D9_QUALIFIED_CRATE, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA using crate:: qualified constant + let (pda, _) = Pubkey::find_program_address(&[D9_QUALIFIED_CRATE], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9QualifiedCrate { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9QualifiedCrate { + _params: D9QualifiedCrateParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9QualifiedCrate instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9QualifiedDeep: Deeply nested crate path +#[tokio::test] +async fn test_d9_qualified_deep() { + use csdk_anchor_full_derived_test::{d9_seeds::D9QualifiedDeepParams, D9_CONSTANT_SEED}; + + let mut ctx = TestContext::new().await; + + // Derive PDA using deeply nested crate path + let (pda, _) = Pubkey::find_program_address(&[D9_CONSTANT_SEED], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9QualifiedDeep { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9QualifiedDeep { + _params: D9QualifiedDeepParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9QualifiedDeep instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9QualifiedMixed: Mixed qualified and bare paths in same seeds +#[tokio::test] +async fn test_d9_qualified_mixed() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9QualifiedMixedParams, + instructions::d9_seeds::qualified_paths::D9_QUALIFIED_LOCAL, D9_CONSTANT_SEED, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA using mixed paths + let (pda, _) = Pubkey::find_program_address( + &[D9_QUALIFIED_LOCAL, D9_CONSTANT_SEED, owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9QualifiedMixed { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9QualifiedMixed { + params: D9QualifiedMixedParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9QualifiedMixed instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +// ============================================================================= +// D9 Method Chains Tests +// ============================================================================= + +/// Tests D9MethodAsRef: constant.as_ref() +#[tokio::test] +async fn test_d9_method_as_ref() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9MethodAsRefParams, instructions::d9_seeds::method_chains::D9_METHOD_BYTES, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[D9_METHOD_BYTES.as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9MethodAsRef { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9MethodAsRef { + _params: D9MethodAsRefParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9MethodAsRef instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9MethodAsBytes: string_constant.as_bytes() +#[tokio::test] +async fn test_d9_method_as_bytes() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9MethodAsBytesParams, instructions::d9_seeds::method_chains::D9_METHOD_STR, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[D9_METHOD_STR.as_bytes()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9MethodAsBytes { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9MethodAsBytes { + _params: D9MethodAsBytesParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9MethodAsBytes instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9MethodQualifiedAsBytes: crate::path::CONST.as_bytes() +#[tokio::test] +async fn test_d9_method_qualified_as_bytes() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9MethodQualifiedAsBytesParams, + instructions::d9_seeds::method_chains::D9_METHOD_STR, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[D9_METHOD_STR.as_bytes()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9MethodQualifiedAsBytes { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9MethodQualifiedAsBytes { + _params: D9MethodQualifiedAsBytesParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9MethodQualifiedAsBytes instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9MethodToLeBytes: params.field.to_le_bytes().as_ref() +#[tokio::test] +async fn test_d9_method_to_le_bytes() { + use csdk_anchor_full_derived_test::d9_seeds::D9MethodToLeBytesParams; + + let mut ctx = TestContext::new().await; + let id = 12345u64; + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[b"d9_le", id.to_le_bytes().as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9MethodToLeBytes { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9MethodToLeBytes { + _params: D9MethodToLeBytesParams { + create_accounts_proof: proof_result.create_accounts_proof, + id, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9MethodToLeBytes instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9MethodToBeBytes: params.field.to_be_bytes().as_ref() +#[tokio::test] +async fn test_d9_method_to_be_bytes() { + use csdk_anchor_full_derived_test::d9_seeds::D9MethodToBeBytesParams; + + let mut ctx = TestContext::new().await; + let id = 67890u64; + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[b"d9_be", id.to_be_bytes().as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9MethodToBeBytes { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9MethodToBeBytes { + _params: D9MethodToBeBytesParams { + create_accounts_proof: proof_result.create_accounts_proof, + id, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9MethodToBeBytes instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9MethodMixed: Mixed methods in seeds +#[tokio::test] +async fn test_d9_method_mixed() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9MethodMixedParams, instructions::d9_seeds::method_chains::D9_METHOD_STR, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + let id = 11111u64; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[ + D9_METHOD_STR.as_bytes(), + owner.as_ref(), + id.to_le_bytes().as_ref(), + ], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9MethodMixed { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9MethodMixed { + params: D9MethodMixedParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + id, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9MethodMixed instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +// ============================================================================= +// D9 Array Bumps Tests +// ============================================================================= + +/// Tests D9BumpLiteral: Literal seed with bump +#[tokio::test] +async fn test_d9_bump_literal() { + use csdk_anchor_full_derived_test::d9_seeds::D9BumpLiteralParams; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[b"d9_bump_lit"], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9BumpLiteral { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9BumpLiteral { + _params: D9BumpLiteralParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9BumpLiteral instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9BumpConstant: Constant seed with bump +#[tokio::test] +async fn test_d9_bump_constant() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9BumpConstantParams, instructions::d9_seeds::array_bumps::D9_BUMP_SEED, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[D9_BUMP_SEED], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9BumpConstant { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9BumpConstant { + _params: D9BumpConstantParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9BumpConstant instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9BumpQualified: Qualified path with bump +#[tokio::test] +async fn test_d9_bump_qualified() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9BumpQualifiedParams, instructions::d9_seeds::array_bumps::D9_BUMP_STR, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[D9_BUMP_STR.as_bytes()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9BumpQualified { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9BumpQualified { + _params: D9BumpQualifiedParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9BumpQualified instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9BumpParam: Param seed with bump +#[tokio::test] +async fn test_d9_bump_param() { + use csdk_anchor_full_derived_test::d9_seeds::D9BumpParamParams; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[b"d9_bump_param", owner.as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9BumpParam { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9BumpParam { + params: D9BumpParamParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9BumpParam instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9BumpCtx: Ctx account seed with bump +#[tokio::test] +async fn test_d9_bump_ctx() { + use csdk_anchor_full_derived_test::d9_seeds::D9BumpCtxParams; + + let mut ctx = TestContext::new().await; + let authority = Keypair::new(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[b"d9_bump_ctx", authority.pubkey().as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9BumpCtx { + fee_payer: ctx.payer.pubkey(), + authority: authority.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9BumpCtx { + _params: D9BumpCtxParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9BumpCtx instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9BumpMixed: Multiple seeds with bump +#[tokio::test] +async fn test_d9_bump_mixed() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9BumpMixedParams, instructions::d9_seeds::array_bumps::D9_BUMP_SEED, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + let id = 54321u64; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[ + b"d9_bump_mix", + D9_BUMP_SEED, + owner.as_ref(), + id.to_le_bytes().as_ref(), + ], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9BumpMixed { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9BumpMixed { + params: D9BumpMixedParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + id, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9BumpMixed instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +// ============================================================================= +// D9 Complex Mixed Tests +// ============================================================================= + +/// Tests D9ComplexThree: 3 seeds - literal + constant + param +#[tokio::test] +async fn test_d9_complex_three() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9ComplexThreeParams, instructions::d9_seeds::complex_mixed::D9_COMPLEX_PREFIX, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[b"d9_complex3", D9_COMPLEX_PREFIX, owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ComplexThree { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ComplexThree { + params: D9ComplexThreeParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ComplexThree instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ComplexFour: 4 seeds - version + namespace + param + bytes +#[tokio::test] +async fn test_d9_complex_four() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9ComplexFourParams, + instructions::d9_seeds::complex_mixed::{D9_COMPLEX_NAMESPACE, D9_COMPLEX_V1}, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + let id = 99999u64; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[ + D9_COMPLEX_V1, + D9_COMPLEX_NAMESPACE.as_bytes(), + owner.as_ref(), + id.to_le_bytes().as_ref(), + ], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ComplexFour { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ComplexFour { + params: D9ComplexFourParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + id, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ComplexFour instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ComplexFive: 5 seeds with ctx account +#[tokio::test] +async fn test_d9_complex_five() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9ComplexFiveParams, + instructions::d9_seeds::complex_mixed::{D9_COMPLEX_NAMESPACE, D9_COMPLEX_V1}, + }; + + let mut ctx = TestContext::new().await; + let authority = Keypair::new(); + let owner = Keypair::new().pubkey(); + let id = 88888u64; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[ + D9_COMPLEX_V1, + D9_COMPLEX_NAMESPACE.as_bytes(), + authority.pubkey().as_ref(), + owner.as_ref(), + id.to_le_bytes().as_ref(), + ], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ComplexFive { + fee_payer: ctx.payer.pubkey(), + authority: authority.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ComplexFive { + params: D9ComplexFiveParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + id, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ComplexFive instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ComplexQualifiedMix: Qualified paths mixed with local +#[tokio::test] +async fn test_d9_complex_qualified_mix() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9ComplexQualifiedMixParams, + instructions::d9_seeds::complex_mixed::{D9_COMPLEX_PREFIX, D9_COMPLEX_V1}, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[D9_COMPLEX_V1, D9_COMPLEX_PREFIX, owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ComplexQualifiedMix { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ComplexQualifiedMix { + params: D9ComplexQualifiedMixParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ComplexQualifiedMix instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ComplexFunc: Function call combined with other seeds +#[tokio::test] +async fn test_d9_complex_func() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9ComplexFuncParams, instructions::d9_seeds::complex_mixed::D9_COMPLEX_V1, + }; + + let mut ctx = TestContext::new().await; + let key_a = Keypair::new().pubkey(); + let key_b = Keypair::new().pubkey(); + let id = 77777u64; + + // Derive PDA using max_key + let max_key = csdk_anchor_full_derived_test::max_key(&key_a, &key_b); + let (pda, _) = Pubkey::find_program_address( + &[D9_COMPLEX_V1, max_key.as_ref(), id.to_le_bytes().as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ComplexFunc { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ComplexFunc { + params: D9ComplexFuncParams { + create_accounts_proof: proof_result.create_accounts_proof, + key_a, + key_b, + id, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ComplexFunc instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ComplexAllQualified: All paths being fully qualified +#[tokio::test] +async fn test_d9_complex_all_qualified() { + use csdk_anchor_full_derived_test::{ + d9_seeds::D9ComplexAllQualifiedParams, + instructions::d9_seeds::complex_mixed::{D9_COMPLEX_NAMESPACE, D9_COMPLEX_V1}, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[ + D9_COMPLEX_V1, + D9_COMPLEX_NAMESPACE.as_bytes(), + owner.as_ref(), + ], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ComplexAllQualified { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ComplexAllQualified { + params: D9ComplexAllQualifiedParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ComplexAllQualified instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ComplexProgramId: Program ID as seed +#[tokio::test] +async fn test_d9_complex_program_id() { + use csdk_anchor_full_derived_test::d9_seeds::D9ComplexProgramIdParams; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA using program ID + let (pda, _) = Pubkey::find_program_address( + &[b"d9_progid", ctx.program_id.as_ref(), owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ComplexProgramId { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ComplexProgramId { + params: D9ComplexProgramIdParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ComplexProgramId instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ComplexIdFunc: id() function call as seed +#[tokio::test] +async fn test_d9_complex_id_func() { + use csdk_anchor_full_derived_test::d9_seeds::D9ComplexIdFuncParams; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA using id() function (same result as program ID) + let (pda, _) = Pubkey::find_program_address( + &[b"d9_idfunc", ctx.program_id.as_ref(), owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ComplexIdFunc { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ComplexIdFunc { + params: D9ComplexIdFuncParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ComplexIdFunc instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +// ============================================================================= +// D9 Edge Cases Tests +// ============================================================================= + +/// Tests D9EdgeEmpty: Empty literal placeholder +#[tokio::test] +async fn test_d9_edge_empty() { + use csdk_anchor_full_derived_test::d9_seeds::D9EdgeEmptyParams; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[&b"d9_edge_empty"[..], &b"_"[..], owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9EdgeEmpty { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9EdgeEmpty { + params: D9EdgeEmptyParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9EdgeEmpty instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9EdgeSingleByte: Single byte constant +#[tokio::test] +async fn test_d9_edge_single_byte() { + use csdk_anchor_full_derived_test::d9_seeds::{ + edge_cases::D9_SINGLE_BYTE, D9EdgeSingleByteParams, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[D9_SINGLE_BYTE], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9EdgeSingleByte { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9EdgeSingleByte { + _params: D9EdgeSingleByteParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9EdgeSingleByte instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9EdgeSingleLetter: Single letter constant name +#[tokio::test] +async fn test_d9_edge_single_letter() { + use csdk_anchor_full_derived_test::d9_seeds::{edge_cases::A, D9EdgeSingleLetterParams}; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[A], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9EdgeSingleLetter { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9EdgeSingleLetter { + _params: D9EdgeSingleLetterParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9EdgeSingleLetter instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9EdgeDigits: Constant name with digits +#[tokio::test] +async fn test_d9_edge_digits() { + use csdk_anchor_full_derived_test::d9_seeds::{edge_cases::SEED_123, D9EdgeDigitsParams}; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[SEED_123], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9EdgeDigits { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9EdgeDigits { + _params: D9EdgeDigitsParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9EdgeDigits instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9EdgeUnderscore: Leading underscore constant +#[tokio::test] +async fn test_d9_edge_underscore() { + use csdk_anchor_full_derived_test::d9_seeds::{ + edge_cases::_UNDERSCORE_CONST, D9EdgeUnderscoreParams, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[_UNDERSCORE_CONST], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9EdgeUnderscore { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9EdgeUnderscore { + _params: D9EdgeUnderscoreParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9EdgeUnderscore instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9EdgeManyLiterals: Many literals in seeds +#[tokio::test] +async fn test_d9_edge_many_literals() { + use csdk_anchor_full_derived_test::d9_seeds::D9EdgeManyLiteralsParams; + + let mut ctx = TestContext::new().await; + + // Derive PDA with 5 byte literals + let (pda, _) = Pubkey::find_program_address(&[b"a", b"b", b"c", b"d", b"e"], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9EdgeManyLiterals { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9EdgeManyLiterals { + _params: D9EdgeManyLiteralsParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9EdgeManyLiterals instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9EdgeMixed: Mixed edge cases +#[tokio::test] +async fn test_d9_edge_mixed() { + use csdk_anchor_full_derived_test::d9_seeds::{ + edge_cases::{A, SEED_123, _UNDERSCORE_CONST}, + D9EdgeMixedParams, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[A, SEED_123, _UNDERSCORE_CONST, owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9EdgeMixed { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9EdgeMixed { + params: D9EdgeMixedParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9EdgeMixed instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +// ============================================================================= +// D9 External Paths Tests +// ============================================================================= + +/// Tests D9ExternalSdkTypes: External crate (light_sdk_types) +#[tokio::test] +async fn test_d9_external_sdk_types() { + use csdk_anchor_full_derived_test::d9_seeds::D9ExternalSdkTypesParams; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA using external constant + let (pda, _) = Pubkey::find_program_address( + &[ + b"d9_ext_sdk", + light_sdk_types::constants::CPI_AUTHORITY_PDA_SEED, + owner.as_ref(), + ], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ExternalSdkTypes { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ExternalSdkTypes { + params: D9ExternalSdkTypesParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ExternalSdkTypes instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ExternalCtoken: External crate (light_token_types) +#[tokio::test] +async fn test_d9_external_ctoken() { + use csdk_anchor_full_derived_test::d9_seeds::D9ExternalCtokenParams; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA using external constant + let (pda, _) = Pubkey::find_program_address( + &[ + b"d9_ext_ctoken", + light_token_interface::POOL_SEED, + owner.as_ref(), + ], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ExternalCtoken { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ExternalCtoken { + params: D9ExternalCtokenParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ExternalCtoken instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ExternalMixed: Multiple external crates mixed +#[tokio::test] +async fn test_d9_external_mixed() { + use csdk_anchor_full_derived_test::d9_seeds::D9ExternalMixedParams; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA using mixed external constants + let (pda, _) = Pubkey::find_program_address( + &[ + light_sdk_types::constants::CPI_AUTHORITY_PDA_SEED, + light_token_interface::POOL_SEED, + owner.as_ref(), + ], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ExternalMixed { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ExternalMixed { + params: D9ExternalMixedParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ExternalMixed instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ExternalWithLocal: External with local constant +#[tokio::test] +async fn test_d9_external_with_local() { + use csdk_anchor_full_derived_test::d9_seeds::{ + external_paths::D9_EXTERNAL_LOCAL, D9ExternalWithLocalParams, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[ + D9_EXTERNAL_LOCAL, + light_sdk_types::constants::RENT_SPONSOR_SEED, + owner.as_ref(), + ], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ExternalWithLocal { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ExternalWithLocal { + params: D9ExternalWithLocalParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ExternalWithLocal instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ExternalBump: External constant with bump +#[tokio::test] +async fn test_d9_external_bump() { + use csdk_anchor_full_derived_test::d9_seeds::D9ExternalBumpParams; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[light_token_interface::COMPRESSED_MINT_SEED, owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ExternalBump { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ExternalBump { + params: D9ExternalBumpParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ExternalBump instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ExternalReexport: Re-exported external constant +#[tokio::test] +async fn test_d9_external_reexport() { + use csdk_anchor_full_derived_test::d9_seeds::{ + external_paths::REEXPORTED_SEED, D9ExternalReexportParams, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA using re-exported constant + let (pda, _) = Pubkey::find_program_address(&[REEXPORTED_SEED], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ExternalReexport { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ExternalReexport { + _params: D9ExternalReexportParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ExternalReexport instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +// ============================================================================= +// D9 Nested Seeds Tests +// ============================================================================= + +/// Tests D9NestedSimple: Simple nested struct access +#[tokio::test] +async fn test_d9_nested_simple() { + use csdk_anchor_full_derived_test::d9_seeds::{ + nested_seeds::InnerNested, D9NestedSimpleParams, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[b"d9_nested_simple", owner.as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9NestedSimple { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9NestedSimple { + params: D9NestedSimpleParams { + create_accounts_proof: proof_result.create_accounts_proof, + nested: InnerNested { owner, id: 0 }, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9NestedSimple instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9NestedDouble: Double nested struct access +#[tokio::test] +async fn test_d9_nested_double() { + use csdk_anchor_full_derived_test::d9_seeds::{ + nested_seeds::{InnerNested, OuterNested}, + D9NestedDoubleParams, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[b"d9_nested_double", owner.as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9NestedDouble { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9NestedDouble { + params: D9NestedDoubleParams { + create_accounts_proof: proof_result.create_accounts_proof, + outer: OuterNested { + array: [0; 16], + nested: InnerNested { owner, id: 0 }, + }, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9NestedDouble instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9NestedArrayField: Nested array field access +#[tokio::test] +async fn test_d9_nested_array_field() { + use csdk_anchor_full_derived_test::d9_seeds::{ + nested_seeds::{InnerNested, OuterNested}, + D9NestedArrayFieldParams, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + let array = [1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[b"d9_nested_array", array.as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9NestedArrayField { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9NestedArrayField { + params: D9NestedArrayFieldParams { + create_accounts_proof: proof_result.create_accounts_proof, + outer: OuterNested { + array, + nested: InnerNested { owner, id: 0 }, + }, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9NestedArrayField instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ArrayIndex: Array indexing params.arrays[2].as_slice() +#[tokio::test] +async fn test_d9_array_index() { + use csdk_anchor_full_derived_test::d9_seeds::D9ArrayIndexParams; + + let mut ctx = TestContext::new().await; + + // Create 2D array with deterministic values + let mut arrays = [[0u8; 16]; 10]; + arrays[2] = [42u8; 16]; // The indexed array + + // Derive PDA using the indexed array + let (pda, _) = + Pubkey::find_program_address(&[b"d9_array_idx", arrays[2].as_slice()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ArrayIndex { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ArrayIndex { + _params: D9ArrayIndexParams { + create_accounts_proof: proof_result.create_accounts_proof, + arrays, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ArrayIndex instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9NestedBytes: Nested struct with bytes conversion +#[tokio::test] +async fn test_d9_nested_bytes() { + use csdk_anchor_full_derived_test::d9_seeds::{nested_seeds::InnerNested, D9NestedBytesParams}; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + let id = 123456u64; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[b"d9_nested_bytes", id.to_le_bytes().as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9NestedBytes { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9NestedBytes { + params: D9NestedBytesParams { + create_accounts_proof: proof_result.create_accounts_proof, + nested: InnerNested { owner, id }, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9NestedBytes instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9NestedCombined: Multiple nested seeds combined +#[tokio::test] +async fn test_d9_nested_combined() { + use csdk_anchor_full_derived_test::d9_seeds::{ + nested_seeds::{InnerNested, OuterNested}, + D9NestedCombinedParams, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + let array = [7u8; 16]; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[b"d9_nested_combined", array.as_ref(), owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9NestedCombined { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9NestedCombined { + params: D9NestedCombinedParams { + create_accounts_proof: proof_result.create_accounts_proof, + outer: OuterNested { + array, + nested: InnerNested { owner, id: 0 }, + }, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9NestedCombined instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +// ============================================================================= +// D9 Const Patterns Tests +// ============================================================================= + +/// Tests D9AssocConst: Associated constant +#[tokio::test] +async fn test_d9_assoc_const() { + use csdk_anchor_full_derived_test::d9_seeds::{const_patterns::SeedHolder, D9AssocConstParams}; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[SeedHolder::SEED], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9AssocConst { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9AssocConst { + _params: D9AssocConstParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9AssocConst instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9AssocConstMethod: Associated constant with method +#[tokio::test] +async fn test_d9_assoc_const_method() { + use csdk_anchor_full_derived_test::d9_seeds::{ + const_patterns::SeedHolder, D9AssocConstMethodParams, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[SeedHolder::NAMESPACE.as_bytes()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9AssocConstMethod { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9AssocConstMethod { + _params: D9AssocConstMethodParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9AssocConstMethod instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9MultiAssocConst: Multiple associated constants +#[tokio::test] +async fn test_d9_multi_assoc_const() { + use csdk_anchor_full_derived_test::d9_seeds::{ + const_patterns::{AnotherHolder, SeedHolder}, + D9MultiAssocConstParams, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[SeedHolder::SEED, AnotherHolder::PREFIX, owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9MultiAssocConst { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9MultiAssocConst { + params: D9MultiAssocConstParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9MultiAssocConst instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ConstFn: Const fn call +#[tokio::test] +async fn test_d9_const_fn() { + use csdk_anchor_full_derived_test::d9_seeds::{const_patterns::const_seed, D9ConstFnParams}; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[const_seed()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ConstFn { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ConstFn { + _params: D9ConstFnParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ConstFn instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ConstFnGeneric: Const fn with generic +#[tokio::test] +async fn test_d9_const_fn_generic() { + use csdk_anchor_full_derived_test::d9_seeds::{ + const_patterns::identity_seed, D9ConstFnGenericParams, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[identity_seed::<12>(b"generic_seed")], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ConstFnGeneric { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ConstFnGeneric { + _params: D9ConstFnGenericParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ConstFnGeneric instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9TraitAssocConst: Trait associated constant +#[tokio::test] +async fn test_d9_trait_assoc_const() { + use csdk_anchor_full_derived_test::d9_seeds::{ + const_patterns::{HasSeed, SeedHolder}, + D9TraitAssocConstParams, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[::TRAIT_SEED], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9TraitAssocConst { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9TraitAssocConst { + _params: D9TraitAssocConstParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9TraitAssocConst instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9Static: Static variable +#[tokio::test] +async fn test_d9_static() { + use csdk_anchor_full_derived_test::d9_seeds::{const_patterns::STATIC_SEED, D9StaticParams}; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[&STATIC_SEED], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9Static { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9Static { + _params: D9StaticParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9Static instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9QualifiedConstFn: Qualified const fn +#[tokio::test] +async fn test_d9_qualified_const_fn() { + use csdk_anchor_full_derived_test::d9_seeds::{ + const_patterns::const_seed, D9QualifiedConstFnParams, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[const_seed()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9QualifiedConstFn { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9QualifiedConstFn { + _params: D9QualifiedConstFnParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9QualifiedConstFn instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9FullyQualifiedAssoc: Fully qualified associated constant +#[tokio::test] +async fn test_d9_fully_qualified_assoc() { + use csdk_anchor_full_derived_test::d9_seeds::{ + const_patterns::SeedHolder, D9FullyQualifiedAssocParams, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[SeedHolder::SEED], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9FullyQualifiedAssoc { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9FullyQualifiedAssoc { + _params: D9FullyQualifiedAssocParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9FullyQualifiedAssoc instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9FullyQualifiedTrait: Fully qualified trait associated constant +#[tokio::test] +async fn test_d9_fully_qualified_trait() { + use csdk_anchor_full_derived_test::d9_seeds::{ + const_patterns::{HasSeed, SeedHolder}, + D9FullyQualifiedTraitParams, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[::TRAIT_SEED], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9FullyQualifiedTrait { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9FullyQualifiedTrait { + _params: D9FullyQualifiedTraitParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9FullyQualifiedTrait instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9FullyQualifiedGeneric: Fully qualified const fn with generic +#[tokio::test] +async fn test_d9_fully_qualified_generic() { + use csdk_anchor_full_derived_test::d9_seeds::{ + const_patterns::identity_seed, D9FullyQualifiedGenericParams, + }; + + let mut ctx = TestContext::new().await; + + // Derive PDA + let (pda, _) = + Pubkey::find_program_address(&[identity_seed::<10>(b"fq_generic")], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9FullyQualifiedGeneric { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9FullyQualifiedGeneric { + _params: D9FullyQualifiedGenericParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9FullyQualifiedGeneric instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ConstCombined: Combined const patterns +#[tokio::test] +async fn test_d9_const_combined() { + use csdk_anchor_full_derived_test::d9_seeds::{ + const_patterns::{const_seed, SeedHolder}, + D9ConstCombinedParams, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[SeedHolder::SEED, const_seed(), owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ConstCombined { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ConstCombined { + params: D9ConstCombinedParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ConstCombined instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs new file mode 100644 index 0000000000..10575ccabf --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/metadata_test.rs @@ -0,0 +1,347 @@ +//! Integration tests for mint with metadata support in #[light_mint] macro. + +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_compressible_client::{ + decompress_mint::decompress_mint, get_create_accounts_proof, AccountInterfaceExt, + CreateAccountsProofInput, InitializeRentFreeConfig, +}; +use light_macros::pubkey; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, + Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); + +/// Test creating a mint with metadata and full lifecycle. +/// Phase 1: Create mint on-chain with metadata (name, symbol, uri, update_authority, additional_metadata) +/// Phase 2: Warp slots to trigger auto-compression by forester +/// Phase 3: Decompress mint and verify metadata is preserved +#[tokio::test] +async fn test_create_mint_with_metadata() { + use csdk_anchor_full_derived_test::instruction_accounts::{ + CreateMintWithMetadataParams, METADATA_MINT_SIGNER_SEED, + }; + use light_token_sdk::token::{ + find_mint_address as find_cmint_address, COMPRESSIBLE_CONFIG_V1, + RENT_SPONSOR as CTOKEN_RENT_SPONSOR, + }; + + let program_id = csdk_anchor_full_derived_test::ID; + let mut config = ProgramTestConfig::new_v2( + true, + Some(vec![("csdk_anchor_full_derived_test", program_id)]), + ); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let (init_config_ix, config_pda) = InitializeRentFreeConfig::new( + &program_id, + &payer.pubkey(), + &program_data_pda, + RENT_SPONSOR, + payer.pubkey(), + ) + .build(); + + rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + .await + .expect("Initialize config should succeed"); + + let authority = Keypair::new(); + + // Derive PDA for mint signer + let (mint_signer_pda, mint_signer_bump) = Pubkey::find_program_address( + &[METADATA_MINT_SIGNER_SEED, authority.pubkey().as_ref()], + &program_id, + ); + + // Derive mint PDA + let (cmint_pda, _) = find_cmint_address(&mint_signer_pda); + + // Get proof for the mint + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::mint(mint_signer_pda)], + ) + .await + .unwrap(); + + // Define metadata + let name = b"Test Token".to_vec(); + let symbol = b"TEST".to_vec(); + let uri = b"https://example.com/metadata.json".to_vec(); + let additional_metadata = Some(vec![ + light_token_sdk::AdditionalMetadata { + key: b"author".to_vec(), + value: b"Light Protocol".to_vec(), + }, + light_token_sdk::AdditionalMetadata { + key: b"version".to_vec(), + value: b"1.0.0".to_vec(), + }, + ]); + + let accounts = csdk_anchor_full_derived_test::accounts::CreateMintWithMetadata { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_signer: mint_signer_pda, + cmint: cmint_pda, + compression_config: config_pda, + ctoken_compressible_config: COMPRESSIBLE_CONFIG_V1, + ctoken_rent_sponsor: CTOKEN_RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + ctoken_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::CreateMintWithMetadata { + params: CreateMintWithMetadataParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump, + name: name.clone(), + symbol: symbol.clone(), + uri: uri.clone(), + additional_metadata: additional_metadata.clone(), + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateMintWithMetadata should succeed"); + + // Verify mint exists on-chain + let cmint_account = rpc + .get_account(cmint_pda) + .await + .unwrap() + .expect("Mint should exist on-chain"); + + // Parse and verify mint data + use light_token_interface::state::Mint; + let mint: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_account.data[..]) + .expect("Failed to deserialize Mint"); + + // Verify decimals match what was specified in #[light_mint] + assert_eq!(mint.base.decimals, 9, "Mint should have 9 decimals"); + + // Verify mint authority + assert_eq!( + mint.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint authority should be fee_payer" + ); + + // Verify token metadata extension + use light_token_interface::state::extensions::ExtensionStruct; + let extensions = mint.extensions.expect("Mint should have extensions"); + + // Find TokenMetadata extension + let token_metadata = extensions + .iter() + .find_map(|ext| { + if let ExtensionStruct::TokenMetadata(tm) = ext { + Some(tm) + } else { + None + } + }) + .expect("Mint should have TokenMetadata extension"); + + // Verify metadata values + assert_eq!(token_metadata.name, name, "Token name should match"); + assert_eq!(token_metadata.symbol, symbol, "Token symbol should match"); + assert_eq!(token_metadata.uri, uri, "Token URI should match"); + + // Verify update authority (stored as Pubkey, not Option) + let expected_update_authority: light_compressed_account::Pubkey = + authority.pubkey().to_bytes().into(); + assert_eq!( + token_metadata.update_authority, expected_update_authority, + "Update authority should be authority signer" + ); + + // Verify additional metadata (stored as Vec, not Option) + let additional = &token_metadata.additional_metadata; + assert_eq!( + additional.len(), + 2, + "Should have 2 additional metadata entries" + ); + assert_eq!(additional[0].key, b"author".to_vec()); + assert_eq!(additional[0].value, b"Light Protocol".to_vec()); + assert_eq!(additional[1].key, b"version".to_vec()); + assert_eq!(additional[1].value, b"1.0.0".to_vec()); + + // Verify compressed address registered + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + let mint_compressed_address = + light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address( + &mint_signer_pda, + &address_tree_pubkey, + ); + let compressed_mint = rpc + .get_compressed_account(mint_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + assert_eq!( + compressed_mint.address.unwrap(), + mint_compressed_address, + "Mint compressed address should be registered" + ); + + // Verify compressed mint account has empty data (decompressed to on-chain) + assert!( + compressed_mint.data.as_ref().unwrap().data.is_empty(), + "Mint compressed data should be empty (decompressed)" + ); + + // Helper functions for lifecycle assertions + async fn assert_onchain_exists(rpc: &mut LightProgramTest, pda: &Pubkey) { + assert!(rpc.get_account(*pda).await.unwrap().is_some()); + } + async fn assert_onchain_closed(rpc: &mut LightProgramTest, pda: &Pubkey) { + let acc = rpc.get_account(*pda).await.unwrap(); + assert!(acc.is_none() || acc.unwrap().lamports == 0); + } + async fn assert_compressed_exists_with_data(rpc: &mut LightProgramTest, addr: [u8; 32]) { + let acc = rpc + .get_compressed_account(addr, None) + .await + .unwrap() + .value + .unwrap(); + assert_eq!(acc.address.unwrap(), addr); + assert!(!acc.data.as_ref().unwrap().data.is_empty()); + } + + // PHASE 2: Warp to trigger auto-compression by forester + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + + // After warp: mint should be closed on-chain + assert_onchain_closed(&mut rpc, &cmint_pda).await; + + // Compressed mint should exist with non-empty data (now compressed) + assert_compressed_exists_with_data(&mut rpc, mint_compressed_address).await; + + // PHASE 3: Decompress mint and verify metadata is preserved + + // Fetch mint interface (unified hot/cold handling) + let mint_interface = rpc + .get_mint_interface(&mint_signer_pda) + .await + .expect("get_mint_interface should succeed"); + assert!(mint_interface.is_cold(), "Mint should be cold after warp"); + + // Create decompression instruction using decompress_mint helper + let decompress_instructions = decompress_mint(&mint_interface, payer.pubkey(), &rpc) + .await + .expect("decompress_mint should succeed"); + + // Should have 1 instruction for mint decompression + assert_eq!( + decompress_instructions.len(), + 1, + "Should have 1 instruction for mint decompression" + ); + + // Execute decompression + rpc.create_and_send_transaction(&decompress_instructions, &payer.pubkey(), &[&payer]) + .await + .expect("Mint decompression should succeed"); + + // Verify mint is back on-chain + assert_onchain_exists(&mut rpc, &cmint_pda).await; + + // Re-parse and verify mint data with metadata preserved + let cmint_account_after = rpc + .get_account(cmint_pda) + .await + .unwrap() + .expect("Mint should exist on-chain after decompression"); + + let mint_after: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_account_after.data[..]) + .expect("Failed to deserialize Mint after decompression"); + + // Verify decimals preserved + assert_eq!( + mint_after.base.decimals, 9, + "Mint should still have 9 decimals after decompression" + ); + + // Verify mint authority preserved + assert_eq!( + mint_after.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint authority should be preserved after decompression" + ); + + // Verify token metadata extension preserved + let extensions_after = mint_after + .extensions + .expect("Mint should still have extensions after decompression"); + + let token_metadata_after = extensions_after + .iter() + .find_map(|ext| { + if let ExtensionStruct::TokenMetadata(tm) = ext { + Some(tm) + } else { + None + } + }) + .expect("Mint should still have TokenMetadata extension after decompression"); + + // Verify all metadata values preserved through compress/decompress cycle + assert_eq!( + token_metadata_after.name, name, + "Token name should be preserved after decompression" + ); + assert_eq!( + token_metadata_after.symbol, symbol, + "Token symbol should be preserved after decompression" + ); + assert_eq!( + token_metadata_after.uri, uri, + "Token URI should be preserved after decompression" + ); + assert_eq!( + token_metadata_after.update_authority, expected_update_authority, + "Update authority should be preserved after decompression" + ); + + // Verify additional metadata preserved + let additional_after = &token_metadata_after.additional_metadata; + assert_eq!( + additional_after.len(), + 2, + "Should still have 2 additional metadata entries after decompression" + ); + assert_eq!(additional_after[0].key, b"author".to_vec()); + assert_eq!(additional_after[0].value, b"Light Protocol".to_vec()); + assert_eq!(additional_after[1].key, b"version".to_vec()); + assert_eq!(additional_after[1].value, b"1.0.0".to_vec()); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/mod.rs new file mode 100644 index 0000000000..765f1880d3 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/mod.rs @@ -0,0 +1 @@ +mod metadata_test; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/mint_tests.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/mint_tests.rs new file mode 100644 index 0000000000..1c8ab3d80d --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/mint_tests.rs @@ -0,0 +1,3 @@ +//! Integration tests for mint functionality. + +mod mint; diff --git a/sdk-tests/sdk-token-test/src/process_create_two_mints.rs b/sdk-tests/sdk-token-test/src/process_create_two_mints.rs index 6c950232ee..6c5e6d76c6 100644 --- a/sdk-tests/sdk-token-test/src/process_create_two_mints.rs +++ b/sdk-tests/sdk-token-test/src/process_create_two_mints.rs @@ -62,6 +62,7 @@ pub fn process_create_mints<'a, 'info>( mint_seed_pubkey: solana_pubkey::Pubkey::new_from_array(m.mint_seed_pubkey.to_bytes()), authority_seeds: None, mint_signer_seeds: None, + token_metadata: None, }) .collect();