From 6f25b67432e4e8e153b75cbdb71299f65d26cf29 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sat, 10 Jan 2026 17:58:38 +0000 Subject: [PATCH 1/9] wip wip add mint support to sdk and macros sync tests, fix program-test compressioninfo parsing refactor macros update test flow wip wip - mix wip - force merge move to preinit wip wip: separate decomp stage separate ata and cmint handling in decompression wip - try atomic decomp wio wip create_pdas_and_mint_auto ref test_create_pdas_and_mint_auto: compress cmint feat(program-test): implement CMint auto-compression in warp_slot_forward - Add compress_cmint_forester() to handle CMint compression via mint_action - Track ACCOUNT_TYPE_MINT accounts in claim_and_compress - Key fix: pass mint: None to tell on-chain to read from CMint Solana account - Update test to rely on auto-compression instead of explicit compression Auto-compress coverage now includes: CToken, Program PDAs, and CMint wip - autocompress fix address derive path fix address derivation for cpdas cleanup basic_test.rs cleanup macro wip works cargo test-sbf attempt to clean up ov works: decompress_accounts_idempotent stage: before macro refactor specs for macro refactor 1 update refactor spec ph1 ph2 wip3 - before ctokenseedprovider decompress refactor macros owrks killed compressible macro cleanup rm non derived cleanup, ctoken cpi clean --- .cargo/config.toml | 3 + Cargo.lock | 80 +- Cargo.toml | 4 + program-libs/compressible/Cargo.toml | 1 + program-libs/compressible/src/lib.rs | 16 + sdk-libs/compressible-client/Cargo.toml | 9 +- sdk-libs/compressible-client/DECOMPRESSION.md | 238 +++ .../compressible-client/decompress-atas.md | 619 ++++++++ .../compressible-client/decompress-mint.md | 514 ++++++ sdk-libs/compressible-client/decompress_ux.md | 272 ++++ sdk-libs/compressible-client/helper.md | 114 ++ sdk-libs/compressible-client/proof_helper.md | 345 +++++ .../src/create_accounts_proof.rs | 207 +++ .../src/decompress_atas.rs | 785 ++++++++++ .../src/decompress_mint.rs | 426 +++++ .../src/initialize_config.rs | 164 ++ sdk-libs/compressible-client/src/lib.rs | 313 +++- sdk-libs/compressible-client/src/pack.rs | 119 ++ sdk-libs/compressible-client/wrapper.md | 1013 ++++++++++++ sdk-libs/macros/MACRO-NEW.md | 792 ++++++++++ sdk-libs/macros/MACRO_REFACTOR.md | 518 +++++++ sdk-libs/macros/MACRO_REFACTOR_V2.md | 642 ++++++++ sdk-libs/macros/OPTION_A_PLAN.md | 404 +++++ sdk-libs/macros/OPTION_A_STATE_FLOW.md | 316 ++++ sdk-libs/macros/OVERVIEW.md | 194 +++ sdk-libs/macros/SPEC_OPTION_A.md | 547 +++++++ sdk-libs/macros/SPEC_OPTION_B.md | 797 ++++++++++ sdk-libs/macros/src/accounts.rs | 641 -------- sdk-libs/macros/src/compressible/GUIDE.md | 198 --- sdk-libs/macros/src/compressible/README.md | 6 +- .../macros/src/compressible/anchor_seeds.rs | 677 ++++++++ .../src/compressible/decompress_context.rs | 201 +-- .../macros/src/compressible/file_scanner.rs | 169 ++ .../macros/src/compressible/instructions.rs | 1260 ++++++++++----- .../src/compressible/light_compressible.rs | 272 ++++ sdk-libs/macros/src/compressible/mod.rs | 3 + .../macros/src/compressible/seed_providers.rs | 587 +++---- sdk-libs/macros/src/compressible/traits.rs | 18 +- .../macros/src/compressible/variant_enum.rs | 234 ++- sdk-libs/macros/src/finalize/codegen.rs | 692 +++++++++ sdk-libs/macros/src/finalize/instruction.rs | 115 ++ sdk-libs/macros/src/finalize/mod.rs | 18 + sdk-libs/macros/src/finalize/parse.rs | 389 +++++ sdk-libs/macros/src/lib.rs | 420 +++-- sdk-libs/macros/src/program.rs | 301 ---- sdk-libs/macros/src/traits.rs | 401 ----- sdk-libs/program-test/src/compressible.rs | 206 ++- .../src/program_test/light_program_test.rs | 274 ++++ .../sdk/src/compressible/compress_account.rs | 1 + .../compressible/compress_account_on_init.rs | 7 + .../src/compressible/decompress_runtime.rs | 131 +- sdk-libs/sdk/src/compressible/finalize.rs | 105 ++ sdk-libs/sdk/src/compressible/mod.rs | 9 +- sdk-libs/sdk/src/compressible/traits.rs | 64 + sdk-libs/sdk/src/lib.rs | 4 +- sdk-libs/token-sdk/Cargo.toml | 2 +- .../compressed_token/v2/decompress_full.rs | 11 +- .../src/compressible/decompress_runtime.rs | 176 ++- sdk-libs/token-sdk/src/pack.rs | 39 +- sdk-libs/token-sdk/src/token/create.rs | 191 ++- sdk-libs/token-sdk/src/token/create_ata.rs | 216 ++- .../token-sdk/src/token/decompress_mint.rs | 242 +++ sdk-libs/token-sdk/src/token/mod.rs | 53 +- sdk-libs/token-sdk/tests/pack_test.rs | 2 +- .../csdk-anchor-derived-test/Anchor.toml | 18 - sdk-tests/csdk-anchor-derived-test/Cargo.toml | 59 - sdk-tests/csdk-anchor-derived-test/Xargo.toml | 4 - .../csdk-anchor-derived-test/package.json | 11 - .../csdk-anchor-derived-test/src/errors.rs | 12 - .../src/instruction_accounts.rs | 109 -- sdk-tests/csdk-anchor-derived-test/src/lib.rs | 291 ---- .../csdk-anchor-derived-test/src/processor.rs | 326 ---- .../csdk-anchor-derived-test/src/seeds.rs | 47 - .../csdk-anchor-derived-test/src/state.rs | 126 -- .../csdk-anchor-derived-test/src/variant.rs | 173 --- .../tests/basic_test.rs | 706 --------- .../csdk-anchor-full-derived-test/SUMMARY.md | 198 +++ .../src/instruction_accounts.rs | 110 +- .../csdk-anchor-full-derived-test/src/lib.rs | 133 +- .../src/state.rs | 62 +- .../tests/basic_test.rs | 551 +++++-- sdk-tests/sdk-compressible-test/Anchor.toml | 19 - sdk-tests/sdk-compressible-test/Cargo.toml | 58 - sdk-tests/sdk-compressible-test/Xargo.toml | 2 - sdk-tests/sdk-compressible-test/package.json | 11 - .../sdk-compressible-test/src/constants.rs | 3 - sdk-tests/sdk-compressible-test/src/errors.rs | 92 -- .../src/instruction_accounts.rs | 223 --- .../compress_accounts_idempotent.rs | 135 -- .../src/instructions/create_game_session.rs | 77 - .../instructions/create_placeholder_record.rs | 68 - .../src/instructions/create_record.rs | 68 - .../create_user_record_and_game_session.rs | 218 --- .../decompress_accounts_idempotent.rs | 472 ------ .../initialize_compression_config.rs | 35 - .../src/instructions/mod.rs | 10 - .../instructions/update_compression_config.rs | 29 - .../src/instructions/update_game_session.rs | 23 - .../src/instructions/update_record.rs | 19 - sdk-tests/sdk-compressible-test/src/lib.rs | 178 --- sdk-tests/sdk-compressible-test/src/seeds.rs | 219 --- sdk-tests/sdk-compressible-test/src/state.rs | 522 ------- .../tests/game_session_tests.rs | 228 --- .../sdk-compressible-test/tests/helpers.rs | 334 ---- .../tests/idempotency_tests.rs | 137 -- .../tests/multi_account_tests.rs | 1379 ----------------- .../tests/placeholder_tests.rs | 527 ------- .../tests/user_record_tests.rs | 293 ---- .../sdk-light-token-test/src/create_ata.rs | 50 +- .../src/create_token_account.rs | 22 +- 110 files changed, 15824 insertions(+), 10350 deletions(-) create mode 100644 sdk-libs/compressible-client/DECOMPRESSION.md create mode 100644 sdk-libs/compressible-client/decompress-atas.md create mode 100644 sdk-libs/compressible-client/decompress-mint.md create mode 100644 sdk-libs/compressible-client/decompress_ux.md create mode 100644 sdk-libs/compressible-client/helper.md create mode 100644 sdk-libs/compressible-client/proof_helper.md create mode 100644 sdk-libs/compressible-client/src/create_accounts_proof.rs create mode 100644 sdk-libs/compressible-client/src/decompress_atas.rs create mode 100644 sdk-libs/compressible-client/src/decompress_mint.rs create mode 100644 sdk-libs/compressible-client/src/initialize_config.rs create mode 100644 sdk-libs/compressible-client/src/pack.rs create mode 100644 sdk-libs/compressible-client/wrapper.md create mode 100644 sdk-libs/macros/MACRO-NEW.md create mode 100644 sdk-libs/macros/MACRO_REFACTOR.md create mode 100644 sdk-libs/macros/MACRO_REFACTOR_V2.md create mode 100644 sdk-libs/macros/OPTION_A_PLAN.md create mode 100644 sdk-libs/macros/OPTION_A_STATE_FLOW.md create mode 100644 sdk-libs/macros/OVERVIEW.md create mode 100644 sdk-libs/macros/SPEC_OPTION_A.md create mode 100644 sdk-libs/macros/SPEC_OPTION_B.md delete mode 100644 sdk-libs/macros/src/accounts.rs delete mode 100644 sdk-libs/macros/src/compressible/GUIDE.md create mode 100644 sdk-libs/macros/src/compressible/anchor_seeds.rs create mode 100644 sdk-libs/macros/src/compressible/file_scanner.rs create mode 100644 sdk-libs/macros/src/compressible/light_compressible.rs create mode 100644 sdk-libs/macros/src/finalize/codegen.rs create mode 100644 sdk-libs/macros/src/finalize/instruction.rs create mode 100644 sdk-libs/macros/src/finalize/mod.rs create mode 100644 sdk-libs/macros/src/finalize/parse.rs delete mode 100644 sdk-libs/macros/src/program.rs delete mode 100644 sdk-libs/macros/src/traits.rs create mode 100644 sdk-libs/sdk/src/compressible/finalize.rs create mode 100644 sdk-libs/sdk/src/compressible/traits.rs delete mode 100644 sdk-tests/csdk-anchor-derived-test/Anchor.toml delete mode 100644 sdk-tests/csdk-anchor-derived-test/Cargo.toml delete mode 100644 sdk-tests/csdk-anchor-derived-test/Xargo.toml delete mode 100644 sdk-tests/csdk-anchor-derived-test/package.json delete mode 100644 sdk-tests/csdk-anchor-derived-test/src/errors.rs delete mode 100644 sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs delete mode 100644 sdk-tests/csdk-anchor-derived-test/src/lib.rs delete mode 100644 sdk-tests/csdk-anchor-derived-test/src/processor.rs delete mode 100644 sdk-tests/csdk-anchor-derived-test/src/seeds.rs delete mode 100644 sdk-tests/csdk-anchor-derived-test/src/state.rs delete mode 100644 sdk-tests/csdk-anchor-derived-test/src/variant.rs delete mode 100644 sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md delete mode 100644 sdk-tests/sdk-compressible-test/Anchor.toml delete mode 100644 sdk-tests/sdk-compressible-test/Cargo.toml delete mode 100644 sdk-tests/sdk-compressible-test/Xargo.toml delete mode 100644 sdk-tests/sdk-compressible-test/package.json delete mode 100644 sdk-tests/sdk-compressible-test/src/constants.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/errors.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instruction_accounts.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/create_record.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/mod.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/instructions/update_record.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/lib.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/seeds.rs delete mode 100644 sdk-tests/sdk-compressible-test/src/state.rs delete mode 100644 sdk-tests/sdk-compressible-test/tests/game_session_tests.rs delete mode 100644 sdk-tests/sdk-compressible-test/tests/helpers.rs delete mode 100644 sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs delete mode 100644 sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs delete mode 100644 sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs delete mode 100644 sdk-tests/sdk-compressible-test/tests/user_record_tests.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index c00d2a43f4..018fc87b4b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,9 @@ [alias] xtask = "run --package xtask --" +[resolver] +incompatible-rust-versions = "fallback" + # On Windows # ``` # cargo install -f cargo-binutils diff --git a/Cargo.lock b/Cargo.lock index e465ced71e..fc91d7e30d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1633,41 +1633,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "csdk-anchor-derived-test" -version = "0.1.0" -dependencies = [ - "anchor-lang", - "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9)", - "bincode", - "borsh 0.10.4", - "light-client", - "light-compressed-account", - "light-compressible", - "light-compressible-client", - "light-hasher", - "light-macros", - "light-program-test", - "light-sdk", - "light-sdk-macros", - "light-sdk-types", - "light-test-utils", - "light-token-client", - "light-token-interface", - "light-token-sdk", - "light-token-types", - "solana-account", - "solana-instruction", - "solana-keypair", - "solana-logger", - "solana-program", - "solana-pubkey 2.4.0", - "solana-sdk", - "solana-signature", - "solana-signer", - "tokio", -] - [[package]] name = "csdk-anchor-full-derived-test" version = "0.1.0" @@ -3701,6 +3666,7 @@ dependencies = [ "light-heap", "light-macros", "light-program-profiler", + "light-sdk-types", "light-zero-copy", "pinocchio", "pinocchio-pubkey", @@ -3720,10 +3686,17 @@ dependencies = [ "anchor-lang", "borsh 0.10.4", "light-client", + "light-compressed-account", + "light-compressible", + "light-ctoken-interface", + "light-ctoken-sdk", "light-sdk", "solana-account", "solana-instruction", + "solana-program", + "solana-program-error 2.2.2", "solana-pubkey 2.4.0", + "spl-token-2022 7.0.0", "thiserror 2.0.17", ] @@ -6039,42 +6012,7 @@ dependencies = [ ] [[package]] -name = "sdk-compressible-test" -version = "0.1.0" -dependencies = [ - "anchor-lang", - "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9)", - "bincode", - "borsh 0.10.4", - "light-client", - "light-compressed-account", - "light-compressible", - "light-compressible-client", - "light-hasher", - "light-macros", - "light-program-test", - "light-sdk", - "light-sdk-types", - "light-test-utils", - "light-token-client", - "light-token-interface", - "light-token-sdk", - "light-token-types", - "solana-account", - "solana-instruction", - "solana-keypair", - "solana-logger", - "solana-program", - "solana-pubkey 2.4.0", - "solana-sdk", - "solana-signature", - "solana-signer", - "solana-system-interface 1.0.0", - "tokio", -] - -[[package]] -name = "sdk-light-token-test" +name = "sdk-ctoken-test" version = "0.1.0" dependencies = [ "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 7a715a3dc1..56895bcef3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,9 +55,13 @@ members = [ "sdk-tests/sdk-native-test", "sdk-tests/sdk-v1-native-test", "sdk-tests/sdk-token-test", +<<<<<<< HEAD "sdk-tests/sdk-compressible-test", "sdk-tests/sdk-light-token-test", "sdk-tests/csdk-anchor-derived-test", +======= + "sdk-tests/sdk-ctoken-test", +>>>>>>> a606eb113 (wip) "sdk-tests/csdk-anchor-full-derived-test", "forester-utils", "forester", diff --git a/program-libs/compressible/Cargo.toml b/program-libs/compressible/Cargo.toml index 49491d7cca..f090a5a838 100644 --- a/program-libs/compressible/Cargo.toml +++ b/program-libs/compressible/Cargo.toml @@ -32,6 +32,7 @@ light-program-profiler = { workspace = true } light-heap = { workspace = true, optional = true } light-account-checks = { workspace= true } light-compressed-account = { workspace= true } +light-sdk-types = { workspace = true } aligned-sized = { workspace= true } solana-sysvar = {workspace = true, optional = true} diff --git a/program-libs/compressible/src/lib.rs b/program-libs/compressible/src/lib.rs index 0dff5dfd1f..7800228cb0 100644 --- a/program-libs/compressible/src/lib.rs +++ b/program-libs/compressible/src/lib.rs @@ -8,3 +8,19 @@ pub mod rent; use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; + +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +use light_sdk_types::instruction::PackedAddressTreeInfo; + +/// Proof data for instruction params when creating new compressed accounts. +/// Used in the INIT flow - pass directly to instruction data. +/// All accounts use the same address tree, so only one `address_tree_info` is needed. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct CreateAccountsProof { + /// The validity proof. + pub proof: ValidityProof, + /// Single packed address tree info (all accounts use same tree). + pub address_tree_info: PackedAddressTreeInfo, + /// Output state tree index for new compressed accounts. + pub output_state_tree_index: u8, +} diff --git a/sdk-libs/compressible-client/Cargo.toml b/sdk-libs/compressible-client/Cargo.toml index e148bae684..f775c6e613 100644 --- a/sdk-libs/compressible-client/Cargo.toml +++ b/sdk-libs/compressible-client/Cargo.toml @@ -7,15 +7,22 @@ repository = "https://github.com/lightprotocol/light-protocol" description = "Client instruction builders for Light Protocol compressible accounts" [features] -anchor = ["anchor-lang", "light-sdk/anchor"] +anchor = ["anchor-lang", "light-sdk/anchor", "light-ctoken-sdk/anchor"] [dependencies] solana-instruction = { workspace = true } solana-pubkey = { workspace = true } solana-account = { workspace = true } +solana-program-error = { workspace = true } +solana-program = { workspace = true } +spl-token-2022 = { workspace = true } light-client = { workspace = true, features = ["v2"] } light-sdk = { workspace = true, features = ["v2", "cpi-context"] } +light-ctoken-sdk = { workspace = true, features = ["cpi-context"] } +light-ctoken-interface = { workspace = true } +light-compressed-account = { workspace = true } +light-compressible = { workspace = true } anchor-lang = { workspace = true, features = ["idl-build"], optional = true } borsh = { workspace = true } diff --git a/sdk-libs/compressible-client/DECOMPRESSION.md b/sdk-libs/compressible-client/DECOMPRESSION.md new file mode 100644 index 0000000000..e10b4239df --- /dev/null +++ b/sdk-libs/compressible-client/DECOMPRESSION.md @@ -0,0 +1,238 @@ +# Decompression Client API + +This document describes how to decompress compressed CToken ATAs and CMints. + +## Quick Start + +```rust +use light_compressible_client::{decompress_atas, decompress_cmint}; + +// Decompress ATAs +let atas = vec![ + rpc.get_ata_interface(&mint, &owner).await?, +]; +let instructions = decompress_atas(&atas, fee_payer, &rpc).await?; + +// Decompress CMint +let mint = rpc.get_mint_interface(&signer).await?; +let instructions = decompress_cmint(&mint, fee_payer, &rpc).await?; +``` + +## Unified Token Data + +`AtaInterface` always provides `token_data` regardless of hot/cold state. +Uses the standard `TokenData` type from `light_ctoken_sdk::compat`: + +```rust +let ata = rpc.get_ata_interface(&mint, &owner).await?; + +// Always works - token_data is populated from on-chain or compressed source +println!("Amount: {}", ata.token_data.amount); // Direct field access +println!("Amount: {}", ata.amount()); // Convenience method +println!("Delegate: {:?}", ata.delegate()); + +// Check state +if ata.is_cold() { + // Needs decompression +} else if ata.is_hot() { + // Already on-chain +} else { + // Doesn't exist +} +``` + +## API Overview + +### ATAs + +| Function | Description | +| ------------------------------------------------------------------- | -------------------------------------------- | +| `decompress_atas(&[AtaInterface], fee_payer, &indexer)` | High-perf wrapper: pre-fetch ATAs, call this | +| `build_decompress_atas(&[AtaInterface], fee_payer, proof)` | Sync: caller provides proof | +| `decompress_atas_idempotent(&[(mint, owner)], fee_payer, &indexer)` | Simple: fetches everything | +| `rpc.get_ata_interface(&mint, &owner)` | Fetch ATA state with unified data | + +### CMints + +| Function | Description | +| -------------------------------------------------------------- | -------------------------------------------- | +| `decompress_cmint(&MintInterface, fee_payer, &indexer)` | High-perf wrapper: pre-fetch mint, call this | +| `build_decompress_mint(&MintInterface, fee_payer, proof, ...)` | Sync: caller provides proof | +| `decompress_mint(signer, fee_payer, &indexer)` | Simple: fetches everything | +| `rpc.get_mint_interface(&signer)` | Fetch CMint state | + +## Usage Patterns + +### Pattern 1: Simple (Recommended for most apps) + +Fetches state and proof internally. Easy to use. + +```rust +use light_compressible_client::{decompress_atas_idempotent, decompress_mint}; + +// Decompress ATAs by (mint, owner) pairs +let instructions = decompress_atas_idempotent( + &[(mint1, owner1), (mint2, owner2)], + fee_payer, + &rpc +).await?; + +// Decompress CMint by signer +let instructions = decompress_mint(signer, fee_payer, &rpc).await?; +``` + +### Pattern 2: High-Performance (Recommended for latency-sensitive apps) + +Pre-fetch state, then call lean wrapper. Allows batching state fetches. + +```rust +use light_compressible_client::{decompress_atas, decompress_cmint}; + +// Pre-fetch ATAs (can batch with futures::join_all) +let atas = vec![ + rpc.get_ata_interface(&mint1, &owner1).await?, + rpc.get_ata_interface(&mint2, &owner2).await?, +]; + +// Access data immediately (works for both hot and cold) +for ata in &atas { + println!("ATA {} has {} tokens", ata.ata, ata.amount()); +} + +// Decompress cold ATAs (fetches proof internally, fast-exits if all hot) +let instructions = decompress_atas(&atas, fee_payer, &rpc).await?; + +// Same for mints +let mint = rpc.get_mint_interface(&signer).await?; +let instructions = decompress_cmint(&mint, fee_payer, &rpc).await?; +``` + +### Pattern 3: Maximum Control (For advanced use cases) + +Pre-fetch state AND proof. Fully synchronous instruction building. + +```rust +use light_compressible_client::{build_decompress_atas, build_decompress_mint}; +use light_program_test::Indexer; + +// Pre-fetch ATAs +let atas = vec![ + rpc.get_ata_interface(&mint1, &owner1).await?, + rpc.get_ata_interface(&mint2, &owner2).await?, +]; + +// Check if any cold (sync, instant) +let cold_hashes: Vec<_> = atas.iter().filter_map(|a| a.hash()).collect(); +if cold_hashes.is_empty() { + return Ok(vec![]); // All hot - fast exit +} + +// Get proof (async) +let proof = rpc.get_validity_proof(cold_hashes, vec![], None).await?.value; + +// Build instructions (sync - no RPC) +let instructions = build_decompress_atas(&atas, fee_payer, Some(proof))?; +``` + +## Interface Types + +### AtaInterface + +```rust +pub struct AtaInterface { + pub ata: Pubkey, // ATA pubkey (derived) + pub owner: Pubkey, // Wallet owner (signer) + pub mint: Pubkey, // Token mint + pub bump: u8, // ATA bump + pub is_cold: bool, // Needs decompression? + pub token_data: TokenData, // Always present (standard SPL-compatible type) + pub raw_account: Option, // If hot + pub decompression: Option, // If cold +} + +// Standard TokenData from light_ctoken_sdk::compat (re-exported) +pub struct TokenData { + pub mint: Pubkey, + pub owner: Pubkey, // Note: for ATAs, this is the ATA pubkey + pub amount: u64, + pub delegate: Option, + pub state: AccountState, + pub tlv: Option>, +} + +impl AtaInterface { + fn is_cold(&self) -> bool; // Needs decompression? + fn is_hot(&self) -> bool; // Already on-chain? + fn is_none(&self) -> bool; // Doesn't exist? + fn amount(&self) -> u64; // Convenience accessor + fn delegate(&self) -> Option; // Convenience accessor + fn hash(&self) -> Option<[u8; 32]>; // For proof (if cold) +} +``` + +### MintInterface + +```rust +pub struct MintInterface { + pub cmint: Pubkey, // CMint PDA + pub signer: Pubkey, // Mint signer (seed) + pub address_tree: Pubkey, // Address tree + pub compressed_address: [u8; 32], // Compressed address + pub state: MintState, // Hot/Cold/None +} + +impl MintInterface { + fn is_cold(&self) -> bool; + fn is_hot(&self) -> bool; + fn hash(&self) -> Option<[u8; 32]>; +} +``` + +## Idempotency + +All functions are idempotent: + +- Returns empty `Vec` if account is already on-chain (hot) +- Safe to call multiple times +- No errors for already-decompressed accounts + +## Example: Full Decompression Flow + +```rust +use light_compressible_client::{decompress_atas, decompress_cmint}; + +async fn decompress_all( + rpc: &mut LightProgramTest, + signer: Pubkey, + mint: Pubkey, + owners: &[Pubkey], + fee_payer: Pubkey, + payer: &Keypair, +) -> Result<(), Box> { + // 1. Decompress CMint first (required for ATA decompression) + let mint_interface = rpc.get_mint_interface(&signer).await?; + if mint_interface.is_cold() { + let ix = decompress_cmint(&mint_interface, fee_payer, rpc).await?; + if !ix.is_empty() { + rpc.create_and_send_transaction(&ix, &fee_payer, &[payer]).await?; + } + } + + // 2. Fetch all ATAs (can batch) + let mut atas = Vec::new(); + for owner in owners { + let ata = rpc.get_ata_interface(&mint, owner).await?; + // Data is always available + println!("Owner {} has {} tokens (cold={})", owner, ata.amount(), ata.is_cold()); + atas.push(ata); + } + + // 3. Decompress cold ATAs + let ix = decompress_atas(&atas, fee_payer, rpc).await?; + if !ix.is_empty() { + rpc.create_and_send_transaction(&ix, &fee_payer, &[payer]).await?; + } + + Ok(()) +} +``` diff --git a/sdk-libs/compressible-client/decompress-atas.md b/sdk-libs/compressible-client/decompress-atas.md new file mode 100644 index 0000000000..7346f09292 --- /dev/null +++ b/sdk-libs/compressible-client/decompress-atas.md @@ -0,0 +1,619 @@ +# Decompress ATAs Idempotent Design + +## Overview + +This document describes the SDK-only functionality to decompress multiple ATA-owned compressed token accounts in a single instruction with one proof. + +## Key Facts + +### Can we decompress multiple ATAs in one instruction with one proof? + +**YES**. This is fully supported by the existing `transfer2` instruction. + +**Why:** + +1. `get_validity_proof(hashes: Vec, ...)` accepts multiple account hashes and returns a single ZK proof covering all +2. `decompress_full_ctoken_accounts_with_indices` in `ctoken-sdk` already accepts `&[DecompressFullIndices]` for batched decompress +3. The `is_ata: bool` flag in `DecompressFullIndices` handles the ATA case correctly (owner is not marked as signer) + +### How ATA-owned compressed tokens work + +When a CToken ATA is auto-compressed: + +- The compressed token's `owner` = ATA pubkey (not wallet owner) +- `CompressedOnlyExtension.is_ata = 1` marks it as ATA-owned +- Stored in TLV: `ExtensionStruct::CompressedOnly(CompressedOnlyExtension { is_ata: 1, ... })` + +When querying the indexer: + +- Query by `owner = ATA_pubkey` (not wallet owner) +- ATA pubkey = `derive_ctoken_ata(wallet_owner, mint)` = PDA of `[wallet_owner, CTOKEN_PROGRAM_ID, mint]` + +When decompressing: + +- Wallet owner signs the transaction (not the ATA, which is a PDA) +- `is_ata: true` in `DecompressFullIndices` ensures owner index is NOT marked as signer +- Program verifies ATA derivation: `derive_ctoken_ata(signer, mint) == compressed_owner` + +## Architecture + +### No Macro Support Required + +This is purely SDK/client-side functionality because: + +1. Direct invoke to ctoken program (no CPI from custom program) +2. Wallet owner signs (no program signing/seeds needed) +3. Standard ATA derivation (no custom seeds) +4. Existing `transfer2` instruction handles everything + +### Existing Code Reuse + +| Component | Location | Reuse | +| ----------------------- | -------------------------------------------------------- | ------ | +| ATA derivation | `ctoken-sdk/src/ctoken/create_ata.rs::derive_ctoken_ata` | Direct | +| Decompress full indices | `ctoken-sdk/src/compressed_token/v2/decompress_full.rs` | Direct | +| ATA packing | `pack_for_decompress_full_with_ata` | Direct | +| Transfer2 instruction | `create_transfer2_instruction` | Direct | +| Create ATA idempotent | `CreateAssociatedCTokenAccount::idempotent()` | Direct | +| Validity proof | `light-client::Indexer::get_validity_proof` | Direct | +| Token account query | `get_compressed_token_accounts_by_owner` | Direct | + +## API Design + +### Input: `DecompressAtaRequest` + +```rust +pub struct DecompressAtaRequest { + /// Wallet owner (signer of the transaction) + pub wallet_owner: Pubkey, + /// Token mint + pub mint: Pubkey, + /// Optional: specific compressed token account hashes to decompress + /// If None, decompress all compressed tokens for this ATA + pub hashes: Option>, +} +``` + +### Function Signature + +```rust +/// Decompresses multiple ATA-owned compressed tokens in one instruction. +/// +/// For each (wallet_owner, mint) pair: +/// 1. Derives the ATA address +/// 2. Fetches compressed token accounts owned by that ATA +/// 3. Gets a single validity proof for all accounts +/// 4. Creates destination ATAs if needed (idempotent) +/// 5. Builds single decompress instruction +/// +/// # Arguments +/// * `requests` - List of (wallet_owner, mint) pairs to decompress +/// * `fee_payer` - Fee payer pubkey +/// * `indexer` - Indexer for fetching accounts and proofs +/// +/// # Returns +/// * Vec of instructions: [create_ata_idempotent..., decompress_all] +/// * Returns empty vec if no compressed tokens found +pub async fn decompress_atas_idempotent( + requests: &[DecompressAtaRequest], + fee_payer: Pubkey, + indexer: &I, +) -> Result, CompressibleClientError>; +``` + +### Batching Rules + +1. **Single wallet, multiple mints**: Each mint requires separate ATA, but can share proof +2. **Multiple wallets**: Each wallet must sign, so typically separate transactions +3. **Same ATA, multiple compressed accounts**: Batched into single instruction (common case) + +The common use case is: user has one wallet, multiple compressed token accounts under same ATA, wants to decompress all. + +## Implementation Plan + +### Step 1: Add to `light-compressible-client` + +```rust +// In sdk-libs/compressible-client/src/lib.rs + +pub mod decompress_atas; +pub use decompress_atas::*; +``` + +### Step 2: Core Implementation + +The implementation follows the same pattern as `DecompressToCtoken::instruction()` in `ctoken-sdk/src/ctoken/decompress.rs`: + +```rust +// sdk-libs/compressible-client/src/decompress_atas.rs + +use light_client::indexer::{CompressedTokenAccount, Indexer, ValidityProofWithContext}; +use light_compressed_account::compressed_account::PackedMerkleContext; +use light_ctoken_interface::{ + instructions::{ + extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, + transfer2::MultiInputTokenDataWithContext, + }, + state::{ExtensionStruct, TokenDataVersion}, +}; +use light_ctoken_sdk::{ + compressed_token::{ + v2::transfer2::{ + create_transfer2_instruction, Transfer2AccountsMetaConfig, Transfer2Config, + Transfer2Inputs, + }, + CTokenAccount2, + }, + compat::AccountState, + ctoken::{derive_ctoken_ata, CreateAssociatedCTokenAccount}, +}; +use light_sdk::instruction::{PackedAccounts, PackedStateTreeInfo, ValidityProof}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; + +#[derive(Debug, Clone)] +pub struct DecompressAtaRequest { + pub wallet_owner: Pubkey, + pub mint: Pubkey, + /// Optional: specific hashes to decompress. If None, decompress all. + pub hashes: Option>, +} + +/// Decompresses multiple ATA-owned compressed tokens in one instruction. +/// +/// Returns (create_ata_instructions, decompress_instruction). +/// The decompress instruction is None if no compressed tokens found. +pub async fn decompress_atas_idempotent( + requests: &[DecompressAtaRequest], + fee_payer: Pubkey, + indexer: &I, +) -> Result, CompressibleClientError> { + let mut create_ata_instructions = Vec::new(); + let mut all_accounts: Vec = Vec::new(); + + // Phase 1: Gather compressed token accounts and prepare ATA creation + for request in requests { + let (ata_pubkey, ata_bump) = derive_ctoken_ata(&request.wallet_owner, &request.mint); + + // Query compressed tokens owned by this ATA + let result = indexer + .get_compressed_token_accounts_by_owner(&ata_pubkey, None, None) + .await?; + + let mut accounts = result.value.items; + if accounts.is_empty() { + continue; + } + + // Filter by hashes if specified + if let Some(ref hashes) = request.hashes { + accounts.retain(|acc| hashes.contains(&acc.account.hash)); + } + + if accounts.is_empty() { + continue; + } + + // Create ATA idempotently + let create_ata = CreateAssociatedCTokenAccount::new( + fee_payer, + request.wallet_owner, + request.mint, + ).idempotent().instruction()?; + create_ata_instructions.push(create_ata); + + // Collect context for each account + for acc in accounts { + all_accounts.push(AtaDecompressContext { + token_account: acc, + ata_pubkey, + wallet_owner: request.wallet_owner, + ata_bump, + }); + } + } + + if all_accounts.is_empty() { + return Ok(create_ata_instructions); + } + + // Phase 2: Get validity proof for all accounts + let hashes: Vec<[u8; 32]> = all_accounts + .iter() + .map(|ctx| ctx.token_account.account.hash) + .collect(); + + let proof_result = indexer + .get_validity_proof(hashes, vec![], None) + .await? + .value; + + // Phase 3: Build decompress instruction + let decompress_ix = build_batch_decompress_instruction( + fee_payer, + &all_accounts, + proof_result, + )?; + + let mut instructions = create_ata_instructions; + instructions.push(decompress_ix); + Ok(instructions) +} + +struct AtaDecompressContext { + token_account: CompressedTokenAccount, + ata_pubkey: Pubkey, + wallet_owner: Pubkey, + ata_bump: u8, +} + +fn build_batch_decompress_instruction( + fee_payer: Pubkey, + accounts: &[AtaDecompressContext], + proof: ValidityProofWithContext, +) -> Result { + let mut packed_accounts = PackedAccounts::default(); + + // Pack tree infos first (inserts trees and queues) + let packed_tree_infos = proof.pack_tree_infos(&mut packed_accounts); + let tree_infos = packed_tree_infos.state_trees.as_ref() + .ok_or(CompressibleClientError::NoStateTreesInProof)?; + + let mut token_accounts_vec = Vec::with_capacity(accounts.len()); + let mut in_tlv_data: Vec> = Vec::with_capacity(accounts.len()); + let mut has_any_tlv = false; + + for (i, ctx) in accounts.iter().enumerate() { + let token = &ctx.token_account.token; + let account = &ctx.token_account.account; + let tree_info = &tree_infos.packed_tree_infos[i]; + + // Insert wallet_owner as signer (for ATA, wallet signs, not ATA pubkey) + let owner_index = packed_accounts.insert_or_get_config(ctx.wallet_owner, true, false); + + // Insert ATA pubkey (as the token owner in TokenData - not a signer!) + let ata_index = packed_accounts.insert_or_get(ctx.ata_pubkey); + + // Insert mint + let mint_index = packed_accounts.insert_or_get(token.mint); + + // Insert delegate if present + let delegate_index = token.delegate + .map(|d| packed_accounts.insert_or_get(d)) + .unwrap_or(0); + + // Insert destination ATA + let destination_index = packed_accounts.insert_or_get(ctx.ata_pubkey); + + // Build MultiInputTokenDataWithContext + let source = MultiInputTokenDataWithContext { + owner: ata_index, // Token owner is ATA pubkey (not wallet!) + amount: token.amount, + has_delegate: token.delegate.is_some(), + delegate: delegate_index, + mint: mint_index, + version: TokenDataVersion::ShaFlat as u8, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: tree_info.queue_pubkey_index, + prove_by_index: account.prove_by_index, + leaf_index: account.leaf_index, + }, + root_index: tree_info.root_index, + }; + + // Build CTokenAccount2 for decompress + let mut ctoken_account = CTokenAccount2::new(vec![source.clone()])?; + ctoken_account.decompress_ctoken(token.amount, destination_index)?; + token_accounts_vec.push(ctoken_account); + + // Build TLV for this input (CompressedOnly extension for ATAs) + let is_frozen = token.state == AccountState::Frozen; + let tlv_vec: Vec = token.tlv.as_ref() + .map(|exts| { + exts.iter().filter_map(|ext| match ext { + ExtensionStruct::CompressedOnly(co) => { + Some(ExtensionInstructionData::CompressedOnly( + CompressedOnlyExtensionInstructionData { + delegated_amount: co.delegated_amount, + withheld_transfer_fee: co.withheld_transfer_fee, + is_frozen, + compression_index: 0, + is_ata: true, + bump: ctx.ata_bump, + owner_index, // Wallet owner who signs + } + )) + } + _ => None, + }).collect() + }) + .unwrap_or_default(); + + if !tlv_vec.is_empty() { + has_any_tlv = true; + } + in_tlv_data.push(tlv_vec); + } + + // Convert packed_accounts to AccountMetas + let (packed_account_metas, _, _) = packed_accounts.to_account_metas(); + + // Build Transfer2 instruction + let meta_config = Transfer2AccountsMetaConfig::new(fee_payer, packed_account_metas); + let transfer_config = Transfer2Config::default().filter_zero_amount_outputs(); + + let inputs = Transfer2Inputs { + meta_config, + token_accounts: token_accounts_vec, + transfer_config, + validity_proof: proof.proof, + in_tlv: if has_any_tlv { Some(in_tlv_data) } else { None }, + ..Default::default() + }; + + create_transfer2_instruction(inputs).map_err(CompressibleClientError::from) +} + +#[derive(Debug)] +pub enum CompressibleClientError { + Indexer(light_client::indexer::IndexerError), + CTokenSdk(light_ctoken_sdk::error::CTokenSdkError), + NoStateTreesInProof, + ProgramError(solana_program_error::ProgramError), +} + +impl From for CompressibleClientError { + fn from(e: light_client::indexer::IndexerError) -> Self { + Self::Indexer(e) + } +} + +impl From for CompressibleClientError { + fn from(e: light_ctoken_sdk::error::CTokenSdkError) -> Self { + Self::CTokenSdk(e) + } +} + +impl From for CompressibleClientError { + fn from(e: solana_program_error::ProgramError) -> Self { + Self::ProgramError(e) + } +} + +impl std::fmt::Display for CompressibleClientError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Indexer(e) => write!(f, "Indexer error: {:?}", e), + Self::CTokenSdk(e) => write!(f, "CToken SDK error: {:?}", e), + Self::NoStateTreesInProof => write!(f, "No state trees in proof"), + Self::ProgramError(e) => write!(f, "Program error: {:?}", e), + } + } +} + +impl std::error::Error for CompressibleClientError {} +``` + +### Step 3: Simplified Client API + +For the common case (single wallet, all compressed tokens for an ATA): + +```rust +/// Decompress all compressed tokens for a wallet's ATA +pub async fn decompress_all_for_ata( + wallet_owner: Pubkey, + mint: Pubkey, + fee_payer: Pubkey, + indexer: &I, +) -> Result, CompressibleClientError> { + decompress_atas_idempotent( + &[DecompressAtaRequest { + wallet_owner, + mint, + hashes: None, + }], + fee_payer, + indexer, + ).await +} + +/// Decompress multiple ATAs for multiple mints in one transaction +pub async fn decompress_multiple_atas( + wallet_owner: Pubkey, + mints: &[Pubkey], + fee_payer: Pubkey, + indexer: &I, +) -> Result, CompressibleClientError> { + let requests: Vec<_> = mints + .iter() + .map(|mint| DecompressAtaRequest { + wallet_owner, + mint: *mint, + hashes: None, + }) + .collect(); + + decompress_atas_idempotent(&requests, fee_payer, indexer).await +} +``` + +## Flow Diagram + +``` +User calls decompress_atas_idempotent([{wallet_owner, mint}]) + | + v +derive_ctoken_ata(wallet_owner, mint) -> ata_pubkey + | + v +indexer.get_compressed_token_accounts_by_owner(ata_pubkey) + | + v +[CompressedTokenAccount { owner: ata_pubkey, is_ata: true, ... }] + | + v +indexer.get_validity_proof([hash1, hash2, ...]) -> single proof + | + v +CreateAssociatedCTokenAccount::idempotent() -> create_ata_ix + | + v +decompress_full_ctoken_accounts_with_indices(proof, indices) -> decompress_ix + | + v +Return [create_ata_ix, decompress_ix] +``` + +## Implementation Notes + +### Key Implementation Insight + +For ATA decompress, the compressed token's `owner` field contains the ATA pubkey (not the wallet owner). However: + +1. **The wallet owner signs** the transaction (ATAs are PDAs that cannot sign) +2. **The ATA pubkey goes into TokenData.owner** (for merkle proof verification) +3. **The wallet_owner goes into CompressedOnlyExtension.owner_index** (for ATA derivation verification) + +The ctoken program verifies: `derive_ctoken_ata(owner_from_owner_index, mint) == token_data.owner` + +### Client vs On-chain Distinction + +The implementation uses `create_transfer2_instruction` directly with `Transfer2Inputs` and `CTokenAccount2`, following the same pattern as `DecompressToCtoken::instruction()` in `ctoken-sdk/src/ctoken/decompress.rs`. + +Key differences from on-chain `decompress_full_ctoken_accounts_with_indices`: + +- Uses pubkeys instead of AccountInfo +- Builds AccountMetas via `PackedAccounts::to_account_metas()` +- No CPI needed (direct invoke) + +### Error Handling + +- Return empty vec if no compressed tokens found (idempotent) +- Fail if proof generation fails +- Fail if any individual decompress fails validation + +### Transaction Size Limits + +- Each compressed account adds ~100-150 bytes to instruction data +- Practical limit: ~15-20 accounts per instruction +- For more accounts: split into multiple instructions (still fewer transactions than individual decompress) + +## Testing + +### Test Cases + +1. Single ATA with single compressed token +2. Single ATA with multiple compressed tokens (merge-like) +3. Multiple ATAs for same wallet, different mints +4. ATA already exists (idempotent create) +5. No compressed tokens (returns empty) +6. Mixed: some ATAs have tokens, some don't + +### Example Test + +```rust +#[tokio::test] +async fn test_decompress_ata_idempotent() { + // Setup: Create ATA, mint tokens, warp to compress + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + let mint = /* create mint */; + + // Create ATA and mint some tokens + let (ata_pubkey, _) = derive_ctoken_ata(&payer.pubkey(), &mint); + CreateAssociatedCTokenAccount::new(payer.pubkey(), payer.pubkey(), mint) + .instruction()?.execute(&mut rpc).await?; + + // Mint tokens to ATA + CTokenMintTo { ... }.invoke()?; + + // Warp to auto-compress + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await?; + + // Verify ATA is closed (compressed) + assert!(rpc.get_account(ata_pubkey).await?.is_none()); + + // Verify compressed token exists owned by ATA pubkey + let compressed = rpc.get_compressed_token_accounts_by_owner(&ata_pubkey, None, None).await?; + assert_eq!(compressed.value.items.len(), 1); + assert_eq!(compressed.value.items[0].token.owner, ata_pubkey); + + // DECOMPRESS using the new API + let instructions = decompress_atas_idempotent( + &[DecompressAtaRequest { + wallet_owner: payer.pubkey(), + mint, + hashes: None, + }], + payer.pubkey(), + &rpc, + ).await?; + + // Execute + rpc.create_and_send_transaction(&instructions, &payer.pubkey(), &[&payer]).await?; + + // Verify ATA is back on-chain with balance + let ata_account = rpc.get_account(ata_pubkey).await?.unwrap(); + let ctoken = CToken::deserialize(&mut &ata_account.data[..])?; + assert_eq!(ctoken.amount, expected_amount); + + // Verify no more compressed tokens + let remaining = rpc.get_compressed_token_accounts_by_owner(&ata_pubkey, None, None).await?; + assert!(remaining.value.items.is_empty()); +} +``` + +## Comparison with PDA Decompress + +| Aspect | ATA Decompress | PDA Decompress | +| -------------------- | ------------------ | ------------------------ | +| Invoke type | Direct invoke | CPI from program | +| Signing | Wallet owner signs | Program signs with seeds | +| Seed derivation | Standard ATA | Custom per-program | +| Macro support needed | No | Yes | +| Complexity | Lower | Higher | + +## Files to Create/Modify + +1. **Create**: `sdk-libs/compressible-client/src/decompress_atas.rs` +2. **Modify**: `sdk-libs/compressible-client/src/lib.rs` (add module export) +3. **Test**: Add test in `sdk-tests/` directory + +## Dependencies + +```toml +# In sdk-libs/compressible-client/Cargo.toml +[dependencies] +light-client = { path = "../client" } +light-ctoken-sdk = { path = "../ctoken-sdk" } +light-ctoken-interface = { path = "../../program-libs/ctoken-interface" } +light-compressed-account = { path = "../../program-libs/compressed-account" } +light-sdk = { path = "../sdk" } +solana-pubkey = "2" +solana-instruction = "2" +solana-program-error = "2" +``` + +## What Already Exists vs What to Create + +### Already Exists (Reuse) + +| Function | Location | Purpose | +| ------------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------- | +| `derive_ctoken_ata` | `ctoken-sdk/src/ctoken/create_ata.rs` | Derive ATA address | +| `CreateAssociatedCTokenAccount::idempotent()` | `ctoken-sdk/src/ctoken/create_ata.rs` | Create ATA instruction | +| `create_transfer2_instruction` | `ctoken-sdk/src/compressed_token/v2/transfer2/instruction.rs` | Build transfer2 ix | +| `CTokenAccount2::decompress_ctoken` | `ctoken-sdk/src/compressed_token/v2/account2.rs` | Set decompress mode | +| `ValidityProofWithContext::pack_tree_infos` | `light-client/src/indexer/types.rs` | Pack tree info | +| `PackedAccounts` | `light-sdk/src/instruction/packed_accounts.rs` | Account packing | +| `Transfer2Inputs`, `Transfer2Config`, `Transfer2AccountsMetaConfig` | `ctoken-sdk/src/compressed_token/v2/transfer2/` | Transfer2 config | + +### To Create + +| Function | Location | Purpose | +| ---------------------------- | -------------------------------------------- | --------------- | +| `decompress_atas_idempotent` | `compressible-client/src/decompress_atas.rs` | Main API | +| `decompress_all_for_ata` | `compressible-client/src/decompress_atas.rs` | Convenience API | +| `decompress_multiple_atas` | `compressible-client/src/decompress_atas.rs` | Multi-mint API | +| `CompressibleClientError` | `compressible-client/src/decompress_atas.rs` | Error type | diff --git a/sdk-libs/compressible-client/decompress-mint.md b/sdk-libs/compressible-client/decompress-mint.md new file mode 100644 index 0000000000..ce196a4a27 --- /dev/null +++ b/sdk-libs/compressible-client/decompress-mint.md @@ -0,0 +1,514 @@ +# Decompress Mint SDK Design + +## Overview + +SDK-only functionality to decompress compressed CMint accounts (mints that were created via `#[compressible]` macro and have been auto-compressed by forester). + +## Key Facts + +### Can we build this purely in SDK without macro changes? + +**YES**. This is fully supported by the existing `DecompressCMint` instruction builder in `ctoken-sdk`. + +**Why:** + +1. **DecompressMint is permissionless** - the authority signer is required by the instruction format but NOT validated against `mint_authority`. Anyone can decompress any compressed mint. + +2. **mint_seed does NOT need to sign** - uses `with_mint_signer_no_sign()` internally. The mint_seed is only used for PDA derivation. + +3. `DecompressCMint` struct already exists in `ctoken-sdk/src/ctoken/decompress_cmint.rs` with a complete `instruction()` method. + +4. All data needed is queryable from the indexer via the compressed mint's address. + +### Proof from on-chain code + +From `programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs`: + +```rust +/// DecompressMint is **permissionless** - the caller pays initial rent, rent exemption is sponsored by the rent_sponsor. +/// The authority signer is still required for MintAction, but does not need to match mint_authority. +``` + +From `process_actions.rs` - DecompressMint does NOT call `check_authority()`: + +```rust +ZAction::DecompressMint(decompress_action) => { + // Note: No check_authority() call - permissionless! + process_decompress_mint_action( + decompress_action, + compressed_mint, + validated_accounts, + mint_signer, + fee_payer, + )?; +} +``` + +### How program-created compressed mints work + +When a mint is created via `#[compressible]` macro (like in `csdk-anchor-full-derived-test`): + +1. **mint_seed_pubkey** = A program PDA (e.g., `LP_MINT_SIGNER_SEED + authority`) +2. **CMint PDA** = `find_cmint_address(mint_seed_pubkey)` = PDA of `[COMPRESSED_MINT_SEED, mint_seed_pubkey]` under ctoken program +3. **Compressed address** = `derive_cmint_compressed_address(mint_seed_pubkey, address_tree)` + +When querying the indexer: + +- Query by `address = compressed_address` (derived from mint_seed_pubkey + address_tree) +- OR query by `cmint` pubkey if known + +When decompressing: + +- **Any signer can call** (permissionless) +- mint_seed_pubkey passed for CMint PDA derivation (does NOT sign) +- Fee payer pays for rent and top-up + +## Architecture + +### No CPI Required + +Unlike PDAs which require program signing for decompression, CMint decompression is: + +1. **Direct invoke** to ctoken program +2. **Permissionless** - any signer works as authority +3. **No custom seeds** needed from the caller program + +### Existing Code Reuse + +| Component | Location | Reuse | +| ------------------------------------- | -------------------------------------------------------------------------- | ------ | +| `DecompressCMint` struct | `ctoken-sdk/src/ctoken/decompress_cmint.rs` | Direct | +| `find_cmint_address` | `ctoken-sdk/src/ctoken/create_cmint.rs` | Direct | +| `derive_cmint_compressed_address` | `ctoken-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs` | Direct | +| `CompressedMintWithContext` | `ctoken-interface/src/instructions/mint_action/instruction_data.rs` | Direct | +| `get_compressed_account` | `light-client/src/indexer/indexer_trait.rs` | Direct | +| `get_validity_proof` | `light-client/src/indexer/indexer_trait.rs` | Direct | +| `config_pda()` / `rent_sponsor_pda()` | `ctoken-sdk/src/ctoken/mod.rs` | Direct | + +## API Design + +### Input: `DecompressMintRequest` + +```rust +#[derive(Debug, Clone)] +pub struct DecompressMintRequest { + /// The seed pubkey used to derive the CMint PDA. + /// This is the same value passed as `mint_signer` when the mint was created. + pub mint_seed_pubkey: Pubkey, + /// Address tree where the compressed mint was created. + /// If None, uses the default cmint address tree. + pub address_tree: Option, + /// Rent payment in epochs (must be 0 or >= 2). Default: 2 + pub rent_payment: Option, + /// Lamports for future write operations. Default: 766 + pub write_top_up: Option, +} +``` + +### Primary Function Signature + +```rust +/// Decompresses a compressed CMint to an on-chain CMint Solana account. +/// +/// This is permissionless - any fee_payer can decompress any compressed mint. +/// The mint_seed_pubkey is used to derive the CMint PDA and compressed address. +/// +/// # Arguments +/// * `request` - Decompress mint parameters +/// * `fee_payer` - Fee payer who pays rent and top-up +/// * `indexer` - Indexer for fetching compressed account and proof +/// +/// # Returns +/// * Vec with decompress instruction if mint needs decompressing +/// * Empty vec if mint is already decompressed (idempotent) +/// * Error only for actual failures (not found, indexer errors) +pub async fn decompress_mint_idempotent( + request: DecompressMintRequest, + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressMintError>; +``` + +### Simplified API + +For common case with defaults: + +```rust +/// Decompress a compressed mint with default parameters. +/// +/// Uses default address tree, rent_payment=2, write_top_up=766. +/// Returns empty vec if already decompressed (idempotent). +pub async fn decompress_mint( + mint_seed_pubkey: Pubkey, + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressMintError>; +``` + +### Error Type + +```rust +#[derive(Debug, Error)] +pub enum DecompressMintError { + #[error("Indexer error: {0}")] + Indexer(#[from] IndexerError), + + #[error("Compressed mint not found for seed {mint_seed:?}")] + MintNotFound { mint_seed: Pubkey }, + + #[error("Missing compressed mint data in account")] + MissingMintData, + + #[error("Program error: {0}")] + ProgramError(#[from] ProgramError), +} +``` + +Note: `AlreadyDecompressed` is NOT an error - returns empty vec instead (idempotent behavior). + +## Implementation + +### File: `sdk-libs/compressible-client/src/decompress_mint.rs` + +```rust +use light_client::indexer::{Indexer, IndexerError}; +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +use light_ctoken_interface::{ + instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + state::CompressedMint, + CMINT_ADDRESS_TREE, +}; +use light_ctoken_sdk::ctoken::{ + derive_cmint_compressed_address, DecompressCMint, +}; +use solana_instruction::Instruction; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum DecompressMintError { + #[error("Indexer error: {0}")] + Indexer(#[from] IndexerError), + + #[error("Compressed mint not found for seed {mint_seed:?}")] + MintNotFound { mint_seed: Pubkey }, + + #[error("Missing compressed mint data in account")] + MissingMintData, + + #[error("Program error: {0}")] + ProgramError(#[from] ProgramError), +} + +#[derive(Debug, Clone)] +pub struct DecompressMintRequest { + pub mint_seed_pubkey: Pubkey, + pub address_tree: Option, + pub rent_payment: Option, + pub write_top_up: Option, +} + +impl DecompressMintRequest { + pub fn new(mint_seed_pubkey: Pubkey) -> Self { + Self { + mint_seed_pubkey, + address_tree: None, + rent_payment: None, + write_top_up: None, + } + } + + pub fn with_address_tree(mut self, address_tree: Pubkey) -> Self { + self.address_tree = Some(address_tree); + self + } + + pub fn with_rent_payment(mut self, rent_payment: u8) -> Self { + self.rent_payment = Some(rent_payment); + self + } + + pub fn with_write_top_up(mut self, write_top_up: u32) -> Self { + self.write_top_up = Some(write_top_up); + self + } +} + +/// Default rent payment in epochs (~24 hours per epoch) +pub const DEFAULT_RENT_PAYMENT: u8 = 2; +/// Default write top-up lamports (~3 hours rent per write) +pub const DEFAULT_WRITE_TOP_UP: u32 = 766; + +/// Decompress a compressed mint with default parameters. +/// Returns empty vec if already decompressed (idempotent). +pub async fn decompress_mint( + mint_seed_pubkey: Pubkey, + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressMintError> { + decompress_mint_idempotent(DecompressMintRequest::new(mint_seed_pubkey), fee_payer, indexer) + .await +} + +/// Decompresses a compressed CMint to an on-chain CMint Solana account. +/// +/// This is permissionless - any fee_payer can decompress any compressed mint. +/// Returns empty vec if already decompressed (idempotent). +pub async fn decompress_mint_idempotent( + request: DecompressMintRequest, + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressMintError> { + // 1. Derive addresses + let address_tree = request + .address_tree + .unwrap_or(Pubkey::new_from_array(CMINT_ADDRESS_TREE)); + let compressed_address = + derive_cmint_compressed_address(&request.mint_seed_pubkey, &address_tree); + + // 2. Fetch compressed mint account from indexer + let compressed_account = indexer + .get_compressed_account(compressed_address, None) + .await? + .value + .ok_or(DecompressMintError::MintNotFound { + mint_seed: request.mint_seed_pubkey, + })?; + + // 3. Parse mint data from compressed account + let mint_data = parse_compressed_mint_data(&compressed_account)?; + + // 4. Check if already decompressed - return empty vec (idempotent) + if mint_data.metadata.cmint_decompressed { + return Ok(vec![]); + } + + // 5. Get validity proof + let proof_result = indexer + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await? + .value; + + // 6. Extract tree info from proof result + let account_info = &proof_result.accounts[0]; + let state_tree = account_info.tree_info.tree; + let input_queue = account_info.tree_info.queue; + let output_queue = account_info + .tree_info + .next_tree_info + .as_ref() + .map(|next| next.queue) + .unwrap_or(input_queue); + + // 7. Build CompressedMintWithContext + let mint_instruction_data = CompressedMintInstructionData::try_from(mint_data) + .map_err(|_| DecompressMintError::MissingMintData)?; + + let compressed_mint_with_context = CompressedMintWithContext { + leaf_index: compressed_account.leaf_index, + prove_by_index: compressed_account.prove_by_index, + root_index: account_info + .root_index + .root_index() + .unwrap_or_default(), + address: compressed_address, + mint: Some(mint_instruction_data), + }; + + // 8. Build DecompressCMint instruction + let decompress = DecompressCMint { + mint_seed_pubkey: request.mint_seed_pubkey, + payer: fee_payer, + authority: fee_payer, // Permissionless - any signer works + state_tree, + input_queue, + output_queue, + compressed_mint_with_context, + proof: ValidityProof(proof_result.proof.into()), + rent_payment: request.rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT), + write_top_up: request.write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP), + }; + + let ix = decompress.instruction().map_err(DecompressMintError::from)?; + Ok(vec![ix]) +} + +/// Parse CompressedMint from compressed account data. +fn parse_compressed_mint_data( + account: &light_client::indexer::CompressedAccount, +) -> Result { + use borsh::BorshDeserialize; + + let data = account + .data + .as_ref() + .ok_or(DecompressMintError::MissingMintData)?; + + CompressedMint::try_from_slice(&data.data) + .map_err(|_| DecompressMintError::MissingMintData) +} +``` + +### Module Export + +Update `sdk-libs/compressible-client/src/lib.rs`: + +```rust +pub mod decompress_atas; +pub mod decompress_mint; +pub mod get_compressible_account; + +pub use decompress_atas::*; +pub use decompress_mint::*; +``` + +## Flow Diagram + +``` +User calls decompress_mint(mint_seed_pubkey, fee_payer, indexer) + | + v +derive_cmint_compressed_address(mint_seed_pubkey, address_tree) -> compressed_address + | + v +indexer.get_compressed_account(compressed_address) -> CompressedAccount { data, hash, tree_info } + | + v +parse_compressed_mint_data() -> CompressedMint { metadata.cmint_decompressed? } + | + +--[if cmint_decompressed == true]--> Return empty vec (idempotent) + | + v [if cmint_decompressed == false] +indexer.get_validity_proof([hash]) -> ValidityProofWithContext + | + v +DecompressCMint { + mint_seed_pubkey, + payer: fee_payer, + authority: fee_payer, // Permissionless! + ... +}.instruction() -> Instruction + | + v +Return vec![instruction] (caller signs and sends) +``` + +## Comparison with Other Decompress APIs + +| Aspect | Mint Decompress | ATA Decompress | PDA Decompress | +| --------------- | --------------------------- | ------------------ | ------------------------ | +| Invoke type | Direct invoke | Direct invoke | CPI from program | +| Signing | Permissionless (any signer) | Wallet owner signs | Program signs with seeds | +| Seed derivation | mint_seed -> CMint PDA | Standard ATA | Custom per-program | +| Macro support | No | No | Yes | +| Complexity | Low | Low | Higher | +| Authority check | None | ATA derivation | PDA derivation | + +## Implementation Notes + +### Why Permissionless? + +Decompressing a mint doesn't change ownership or authority - it just brings the data back on-chain. The same mint_authority retains control. Anyone paying rent can "warm up" a cold mint. + +### Idempotency + +The function checks `cmint_decompressed` flag: + +- If false: returns vec with decompress instruction +- If true: returns empty vec (nothing to do) + +This follows the same pattern as `decompress_atas_idempotent` - callers can safely call multiple times without error handling. + +### Transaction Execution + +The returned instructions require **only the fee_payer to sign**: + +```rust +let instructions = decompress_mint(mint_seed, fee_payer, &indexer).await?; +if !instructions.is_empty() { + rpc.create_and_send_transaction(&instructions, &fee_payer, &[&fee_payer_keypair]).await?; +} +// If empty, mint was already decompressed - nothing to do +``` + +### Error Handling + +- `MintNotFound`: Compressed mint doesn't exist (never created or wrong address_tree) +- `AlreadyDecompressed`: Mint is already on-chain (idempotent case) +- `MissingMintData`: Compressed account exists but has no mint data (shouldn't happen) + +## Dependencies + +```toml +# In sdk-libs/compressible-client/Cargo.toml +[dependencies] +light-client = { path = "../client" } +light-ctoken-sdk = { path = "../ctoken-sdk" } +light-ctoken-interface = { path = "../../program-libs/ctoken-interface" } +light-compressed-account = { path = "../../program-libs/compressed-account" } +solana-pubkey = "2" +solana-instruction = "2" +solana-program-error = "2" +thiserror = "1.0" +``` + +## Testing + +### Test Cases + +1. **Basic decompress**: Compressed mint -> on-chain CMint +2. **Already decompressed**: Returns `AlreadyDecompressed` error +3. **Not found**: Returns `MintNotFound` error +4. **Custom address tree**: Works with non-default address tree +5. **Custom rent/top-up**: Respects custom parameters + +### Integration Test Pattern + +```rust +#[tokio::test] +async fn test_decompress_mint() { + // Setup: Create mint via program, warp to compress + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Create mint via csdk-anchor-full-derived-test + let (mint_signer_pda, _) = Pubkey::find_program_address( + &[LP_MINT_SIGNER_SEED, authority.pubkey().as_ref()], + &program_id, + ); + let (cmint_pda, _) = find_cmint_address(&mint_signer_pda); + + // ... execute create_pdas_and_mint_auto ... + + // Verify mint exists on-chain + assert!(rpc.get_account(cmint_pda).await?.is_some()); + + // Warp to auto-compress + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await?; + + // Verify mint is compressed (closed on-chain) + assert!(rpc.get_account(cmint_pda).await?.is_none()); + + // DECOMPRESS using the new API + let instructions = decompress_mint(mint_signer_pda, payer.pubkey(), &rpc).await?; + assert_eq!(instructions.len(), 1); // Should have one decompress instruction + + // Execute (only fee_payer signs) + rpc.create_and_send_transaction(&instructions, &payer.pubkey(), &[&payer]).await?; + + // Verify mint is back on-chain + assert!(rpc.get_account(cmint_pda).await?.is_some()); + + // Verify calling again returns empty vec (idempotent) + let instructions = decompress_mint(mint_signer_pda, payer.pubkey(), &rpc).await?; + assert!(instructions.is_empty()); // Already decompressed, nothing to do +} +``` + +## Files to Create/Modify + +1. **Create**: `sdk-libs/compressible-client/src/decompress_mint.rs` +2. **Modify**: `sdk-libs/compressible-client/src/lib.rs` (add module export) +3. **Modify**: `sdk-libs/compressible-client/Cargo.toml` (add dependencies if missing) +4. **Test**: Add integration test to `sdk-tests/` directory diff --git a/sdk-libs/compressible-client/decompress_ux.md b/sdk-libs/compressible-client/decompress_ux.md new file mode 100644 index 0000000000..3122f32dfd --- /dev/null +++ b/sdk-libs/compressible-client/decompress_ux.md @@ -0,0 +1,272 @@ +# Decompress UX Improvement: `from_seeds` Pattern + +## Problem + +Current decompress flow is verbose and redundant: + +```rust +// 1. Create interface +let user_interface = AccountInterface::cold(user_record_pda, compressed_user.clone()); + +// 2. Extract data, call constructor, pass seeds +let user_variant = CompressedAccountVariant::user_record( + user_interface.compressed_data().unwrap(), // extract data + UserRecordSeeds { authority, mint_authority, owner, category_id }, +)?; + +// 3. Combine interface + variant +RentFreeDecompressAccount::new(user_interface, user_variant) +``` + +**Issues:** +- 3 separate steps per account +- `compressed_data()` extraction is boilerplate +- Interface passed twice conceptually (once for data, once for wrapper) + +## Solution: `from_seeds` Pattern + +```rust +RentFreeDecompressAccount::from_seeds( + AccountInterface::cold(user_record_pda, compressed_user), + UserRecordSeeds { authority, mint_authority, owner, category_id }, +)? +``` + +**Single call.** The `Seeds` type (already generated by macro) tells us which variant to construct. + +## Design + +### 1. Trait Definition (SBF-compatible) + +Location: `light-sdk/src/compressible/mod.rs` + +```rust +/// Trait for seeds that can construct a compressed account variant. +/// Implemented by generated `XxxSeeds` structs. +pub trait IntoVariant { + /// Construct variant from compressed account data bytes and these seeds. + fn into_variant(self, data: &[u8]) -> Result; +} +``` + +This trait is SBF-compatible because: +- No client-crate dependencies +- Just takes `&[u8]` and returns variant +- Lives in `light-sdk` which is already program-side + +### 2. Macro Generates Trait Impl + +Location: `sdk-libs/macros/src/compressible/instructions.rs` + +Currently generates: +```rust +pub struct UserRecordSeeds { + pub authority: Pubkey, + pub mint_authority: Pubkey, + pub owner: Pubkey, + pub category_id: u64, +} + +impl CompressedAccountVariant { + pub fn user_record(data: &[u8], seeds: UserRecordSeeds) -> Result { + // deserialize, verify seeds, construct variant + } +} +``` + +**Add trait impl:** +```rust +impl light_sdk::compressible::IntoVariant for UserRecordSeeds { + fn into_variant(self, data: &[u8]) -> Result { + CompressedAccountVariant::user_record(data, self) + } +} +``` + +### 3. Client Helper Method + +Location: `light-compressible-client/src/lib.rs` + +```rust +impl RentFreeDecompressAccount { + /// Create decompression request from account interface and seeds. + /// + /// The seeds type determines which variant constructor to call. + /// Data is extracted from interface, passed to `IntoVariant::into_variant()`. + pub fn from_seeds( + interface: AccountInterface, + seeds: S, + ) -> Result + where + S: light_sdk::compressible::IntoVariant, + { + let data = interface + .compressed_data() + .ok_or_else(|| anchor_lang::error::Error::from( + anchor_lang::error::ErrorCode::AccountNotInitialized + ))?; + let variant = seeds.into_variant(data)?; + Ok(Self::new(interface, variant)) + } +} +``` + +### 4. CToken Handling + +CToken accounts store `TokenData` in their compressed bytes. Parse internally - no separate `token_data` param needed. + +```rust +impl RentFreeDecompressAccount { + /// Create decompression request for CToken account. + /// Parses TokenData from interface.compressed_data() internally. + pub fn from_ctoken( + interface: AccountInterface, + ctoken_variant: T, + ) -> Result + where + T: IntoCTokenVariant, + { + let data = interface.compressed_data() + .ok_or(Error::AccountNotCompressed)?; + let token_data = TokenData::try_from_slice(data)?; + let variant = ctoken_variant.into_ctoken_variant(token_data); + Ok(Self::new(interface, variant)) + } +} +``` + +**Trait (generated by macro):** +```rust +pub trait IntoCTokenVariant { + fn into_ctoken_variant(self, token_data: TokenData) -> V; +} + +// Generated by macro +impl IntoCTokenVariant for CTokenAccountVariant { + fn into_ctoken_variant(self, token_data: TokenData) -> CompressedAccountVariant { + CompressedAccountVariant::CTokenData(CTokenData { + variant: self, + token_data, + }) + } +} +``` + +Usage: +```rust +RentFreeDecompressAccount::from_ctoken( + AccountInterface::cold(vault_pda, compressed_vault.account), + CTokenAccountVariant::Vault { cmint: cmint_pda }, +)? +``` + +## Final API + +### Before (verbose) +```rust +let user_interface = AccountInterface::cold(user_record_pda, compressed_user.clone()); +let game_interface = AccountInterface::cold(game_session_pda, compressed_game.clone()); +let vault_interface = AccountInterface::cold(vault_pda, compressed_vault.account.clone()); + +let user_variant = CompressedAccountVariant::user_record( + user_interface.compressed_data().unwrap(), + UserRecordSeeds { authority, mint_authority, owner, category_id }, +)?; +let game_variant = CompressedAccountVariant::game_session( + game_interface.compressed_data().unwrap(), + GameSessionSeeds { user, authority, session_id }, +)?; +let vault_ctoken_data = CTokenData { + variant: CTokenAccountVariant::Vault { cmint: cmint_pda }, + token_data: compressed_vault.token.clone(), +}; + +let decompress_accounts = vec![ + RentFreeDecompressAccount::new(user_interface, user_variant), + RentFreeDecompressAccount::new(game_interface, game_variant), + RentFreeDecompressAccount::new(vault_interface, CompressedAccountVariant::CTokenData(vault_ctoken_data)), +]; +``` + +### After (clean) +```rust +let decompress_accounts = vec![ + RentFreeDecompressAccount::from_seeds( + AccountInterface::cold(user_record_pda, compressed_user), + UserRecordSeeds { authority, mint_authority, owner, category_id }, + )?, + RentFreeDecompressAccount::from_seeds( + AccountInterface::cold(game_session_pda, compressed_game), + GameSessionSeeds { user, authority, session_id }, + )?, + RentFreeDecompressAccount::from_ctoken( + AccountInterface::cold(vault_pda, compressed_vault.account), + CTokenAccountVariant::Vault { cmint: cmint_pda }, + )?, +]; +``` + +**Reduction:** ~25 lines → ~12 lines (52% less) +**Cognitive load:** 3 concepts → 1 concept per account +**Redundant data passing:** Eliminated + +## Implementation Checklist + +1. **Add `IntoVariant` trait to `light-sdk`** + - File: `sdk-libs/sdk/src/compressible/mod.rs` + - SBF-compatible, no client deps + +2. **Add `IntoCTokenVariant` trait to `light-sdk`** + - Same file + - For CToken variant construction + +3. **Update macro to generate `IntoVariant` impl** + - File: `sdk-libs/macros/src/compressible/instructions.rs` + - Add impl alongside existing `UserRecordSeeds` struct + +4. **Update macro to generate `IntoCTokenVariant` impl** + - Same file + - For `CTokenAccountVariant` + +5. **Add `from_seeds` method to `RentFreeDecompressAccount`** + - File: `sdk-libs/compressible-client/src/lib.rs` + - Uses trait bound `S: IntoVariant` + +6. **Add `from_ctoken` method to `RentFreeDecompressAccount`** + - Same file + - Uses trait bound `T: IntoCTokenVariant` + +7. **Update test to use new API** + - File: `sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs` + +## Compatibility + +- **SBF programs:** No changes needed. Traits in `light-sdk` are SBF-compatible. +- **Existing code:** `::new()` still works. `from_seeds` is additive. +- **Migration:** Optional. Users can adopt incrementally. + +## Error Handling + +Both methods return `Result` because: + +**`from_seeds`:** +1. `compressed_data()` might be `None` (hot account passed to cold-only method) +2. `into_variant()` can fail (seed verification, deserialization) + +**`from_ctoken`:** +1. `compressed_data()` might be `None` (hot account passed) +2. `TokenData::try_from_slice()` can fail (malformed data) + +## Rating: 9/10 + +### Pros +- **Consistent**: Both use `AccountInterface` first arg +- **Minimal**: Single call per account, no intermediate vars +- **Type-safe**: Traits enforce correct mapping +- **SBF-compatible**: Traits in light-sdk, impl in macro +- **Clear intent**: `from_seeds` vs `from_ctoken` + +### Cons +- CToken still needs `.account` extraction from `CompressedTokenAccount` +- Re-parses `TokenData` from bytes (indexer already parsed, but keeps API uniform) +- Two traits to maintain (hidden from user) diff --git a/sdk-libs/compressible-client/helper.md b/sdk-libs/compressible-client/helper.md new file mode 100644 index 0000000000..8992852849 --- /dev/null +++ b/sdk-libs/compressible-client/helper.md @@ -0,0 +1,114 @@ +# Compressed Account Client Helper + +## The Problem + +Building remaining accounts is verbose and error-prone: + +```rust +// Current: 10+ lines of boilerplate every time +let mut packed = PackedAccounts::default(); +let system_config = match cpi_context { + Some(ctx) => SystemAccountMetaConfig::new_with_cpi_context(program_id, ctx), + None => SystemAccountMetaConfig::new(program_id), +}; +packed.add_system_accounts_v2(system_config)?; +let output_queue = tree_info.next_tree_info.as_ref().map(|n| n.queue).unwrap_or(tree_info.queue); +let output_tree_index = packed.insert_or_get(output_queue); +let packed_trees = proof.pack_tree_infos(&mut packed); +let (remaining_accounts, system_offset, _) = packed.to_account_metas(); +``` + +## The Solution + +One function: + +```rust +pub struct PackedProofResult { + /// Remaining accounts to append to your instruction's accounts. + pub remaining_accounts: Vec, + /// Packed tree infos. Use `.address_trees` or `.state_trees` as needed. + pub packed_tree_infos: PackedTreeInfos, + /// Index of output tree in remaining accounts. + pub output_tree_index: u8, + /// Offset where system accounts start (if needed). + pub system_accounts_offset: u8, +} + +/// Packs validity proof into remaining accounts. +/// +/// # Arguments +/// - `program_id`: Your program ID +/// - `proof`: From `get_validity_proof()` +/// - `output_tree`: From `get_random_state_tree_info()` +/// - `cpi_context`: `tree_info.cpi_context` when mixing PDAs+tokens, else `None` +pub fn pack_proof( + program_id: &Pubkey, + proof: ValidityProofWithContext, + output_tree: &TreeInfo, + cpi_context: Option, +) -> Result; +``` + +## Full Flow + +```rust +// 1. Derive addresses (use existing functions) +let user_addr = derive_address(&user_pda.to_bytes(), &tree.to_bytes(), &program_id.to_bytes()); +let mint_addr = derive_cmint_compressed_address(&mint_signer, &tree); + +// 2. Get proof + output tree +let proof = rpc.get_validity_proof( + vec![], // existing hashes (empty for new accounts) + vec![ // new addresses + AddressWithTree { address: user_addr, tree }, + AddressWithTree { address: mint_addr, tree }, + ], + None, +).await?.value; +let output_tree = rpc.get_random_state_tree_info()?; + +// 3. Pack (the helper) +let packed = pack_proof( + &program_id, + proof.clone(), + &output_tree, + output_tree.cpi_context, // Some for mixed PDA+token, None for PDA-only +)?; + +// 4. Build instruction +let ix = Instruction { + program_id, + accounts: [my_accounts.to_account_metas(None), packed.remaining_accounts].concat(), + data: MyInstruction { + proof: proof.proof, + address_tree_infos: packed.packed_tree_infos.address_trees, + output_tree_index: packed.output_tree_index, + // ... + }.data(), +}; +``` + +## When to use CPI context + +``` +PDA-only tx → cpi_context: None +Token-only tx → cpi_context: None +Mixed PDA + token → cpi_context: tree_info.cpi_context (Option) +``` + +## Errors + +```rust +#[derive(Debug, Error)] +pub enum PackError { + #[error("Failed to add system accounts: {0}")] + SystemAccounts(#[from] LightSdkError), +} +``` + +## Files + +| File | Contents | +| ------------- | ------------------------------------------------ | +| `src/pack.rs` | `pack_proof()`, `PackedProofResult`, `PackError` | +| `src/lib.rs` | Re-export | diff --git a/sdk-libs/compressible-client/proof_helper.md b/sdk-libs/compressible-client/proof_helper.md new file mode 100644 index 0000000000..690b871013 --- /dev/null +++ b/sdk-libs/compressible-client/proof_helper.md @@ -0,0 +1,345 @@ +# get_create_accounts_proof Helper Specification + +## Problem + +Creating compressed accounts (INIT flow) requires verbose boilerplate: + +```rust +// Current: 30+ lines every time +let address_tree_pubkey = rpc.get_address_tree_v2().tree; +let state_tree_info = rpc.get_random_state_tree_info().unwrap(); + +// Derive each address manually +let user_address = derive_address( + &user_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), +); +let game_address = derive_address( + &game_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), +); +let mint_address = derive_cmint_compressed_address(&mint_signer, &address_tree_pubkey); + +// Build AddressWithTree for each +let rpc_result = rpc.get_validity_proof( + vec![], + vec![ + AddressWithTree { address: user_address, tree: address_tree_pubkey }, + AddressWithTree { address: game_address, tree: address_tree_pubkey }, + AddressWithTree { address: mint_address, tree: address_tree_pubkey }, + ], + None, +).await?.value; + +// Pack proof +let packed = pack_proof(&program_id, rpc_result.clone(), &state_tree_info, state_tree_info.cpi_context)?; +let user_tree_info = packed.packed_tree_infos.address_trees[0]; +let game_tree_info = packed.packed_tree_infos.address_trees[1]; +let mint_tree_info = packed.packed_tree_infos.address_trees[2]; +``` + +## Solution + +One opinionated helper for the INIT flow: + +```rust +/// Input for creating new compressed accounts. +/// program_id from main function is used as default owner for Pda variant. +pub enum CreateAccountsProofInput { + /// PDA owned by the calling program (uses program_id from main fn) + Pda(Pubkey), + /// PDA with explicit owner (for cross-program accounts) + PdaWithOwner { pda: Pubkey, owner: Pubkey }, + /// CMint (always uses CTOKEN_PROGRAM_ID internally) + Mint(Pubkey), +} + +impl CreateAccountsProofInput { + /// Standard PDA owned by calling program. + /// Address derived: derive_address(&pda, &tree, &program_id) + pub fn pda(pda: Pubkey) -> Self { + Self::Pda(pda) + } + + /// PDA with explicit owner (rare: cross-program accounts). + /// Address derived: derive_address(&pda, &tree, &owner) + pub fn pda_with_owner(pda: Pubkey, owner: Pubkey) -> Self { + Self::PdaWithOwner { pda, owner } + } + + /// Compressed mint (CMint). + /// Address derived: derive_cmint_compressed_address(&mint_signer, &tree) + pub fn mint(mint_signer: Pubkey) -> Self { + Self::Mint(mint_signer) + } + + /// Derive the compressed address. + fn derive_address(&self, address_tree: &Pubkey, program_id: &Pubkey) -> [u8; 32] { + match self { + Self::Pda(pda) => light_compressed_account::address::derive_address( + &pda.to_bytes(), + &address_tree.to_bytes(), + &program_id.to_bytes(), + ), + Self::PdaWithOwner { pda, owner } => light_compressed_account::address::derive_address( + &pda.to_bytes(), + &address_tree.to_bytes(), + &owner.to_bytes(), + ), + Self::Mint(signer) => derive_cmint_compressed_address(signer, address_tree), + } + } +} +``` + +## Result Type + +```rust +/// Proof data for instruction params. Pass directly to instruction data. +/// All accounts use the same address tree, so only one address_tree_info is needed. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct CreateAccountsProof { + /// The validity proof. + pub proof: ValidityProof, + /// Single packed address tree info (all accounts use same tree). + pub address_tree_info: PackedAddressTreeInfo, + /// Output state tree index for new compressed accounts. + pub output_state_tree_index: u8, +} + +/// Result of get_create_accounts_proof. +pub struct CreateAccountsProofResult { + /// Proof data to include in instruction data. + pub create_accounts_proof: CreateAccountsProof, + /// Remaining accounts to append to instruction accounts. + pub remaining_accounts: Vec, +} +``` + +## Main Function + +````rust +/// Gets validity proof for creating new compressed accounts (INIT flow). +/// +/// Opinionated helper that: +/// - Uses a single address tree (V2) for all addresses +/// - Handles address derivation internally based on input type +/// - Packs proof into remaining accounts +/// +/// # Arguments +/// * `rpc` - RPC client implementing Rpc + Indexer traits +/// * `program_id` - Your program's ID (used as default owner for Pda inputs + system config) +/// * `inputs` - Vec of CreateAccountsProofInput describing accounts to create +/// +/// # Returns +/// CreateAccountsProofResult containing proof, packed tree infos, and remaining accounts. +/// +/// # Example +/// ```rust,ignore +/// let result = get_create_accounts_proof( +/// &rpc, +/// &program_id, +/// vec![ +/// CreateAccountsProofInput::pda(user_pda), +/// CreateAccountsProofInput::pda(game_pda), +/// CreateAccountsProofInput::mint(mint_signer_pda), +/// ], +/// ).await?; +/// +/// // Just pass create_accounts_proof to instruction - macros use defaults +/// let ix = Instruction { +/// program_id, +/// accounts: [my_accounts.to_account_metas(None), result.remaining_accounts].concat(), +/// data: MyInstruction { +/// create_accounts_proof: result.create_accounts_proof, +/// // ... other params +/// }.data(), +/// }; +/// ``` +pub async fn get_create_accounts_proof( + rpc: &R, + program_id: &Pubkey, + inputs: Vec, +) -> Result { + if inputs.is_empty() { + return Err(CreateAccountsProofError::EmptyInputs); + } + + // 1. Get address tree (opinionated: always V2) + let address_tree = rpc.get_address_tree_v2(); + let address_tree_pubkey = address_tree.tree; + + // 2. Derive all compressed addresses (program_id used as default owner for Pda) + let derived_addresses: Vec<[u8; 32]> = inputs + .iter() + .map(|input| input.derive_address(&address_tree_pubkey, program_id)) + .collect(); + + // 3. Build AddressWithTree for each (all use same tree) + let addresses_with_trees: Vec = derived_addresses + .iter() + .map(|&address| AddressWithTree { + address, + tree: address_tree_pubkey, + }) + .collect(); + + // 4. Get validity proof (empty hashes = INIT flow) + let validity_proof = rpc + .get_validity_proof(vec![], addresses_with_trees, None) + .await? + .value; + + // 5. Get output state tree + let state_tree_info = rpc + .get_random_state_tree_info() + .map_err(CreateAccountsProofError::Rpc)?; + + // 6. Determine CPI context + // For INIT with mints: need CPI context for cross-program invocation + let has_mints = inputs.iter().any(|i| matches!(i, CreateAccountsProofInput::Mint(_))); + let cpi_context = if has_mints { + state_tree_info.cpi_context + } else { + None + }; + + // 7. Pack proof + let packed = pack_proof(program_id, validity_proof.clone(), &state_tree_info, cpi_context)?; + + // All addresses use the same tree, so just take the first packed info + let address_tree_info = packed + .packed_tree_infos + .address_trees + .first() + .copied() + .ok_or(CreateAccountsProofError::EmptyInputs)?; + + Ok(CreateAccountsProofResult { + create_accounts_proof: CreateAccountsProof { + proof: validity_proof.proof, + address_tree_info, + output_state_tree_index: packed.output_tree_index, + }, + remaining_accounts: packed.remaining_accounts, + }) +} +```` + +## Error Type + +```rust +#[derive(Debug, Error)] +pub enum CreateAccountsProofError { + #[error("Inputs cannot be empty")] + EmptyInputs, + + #[error("Indexer error: {0}")] + Indexer(#[from] IndexerError), + + #[error("RPC error: {0}")] + Rpc(#[from] RpcError), + + #[error("Pack error: {0}")] + Pack(#[from] PackError), +} +``` + +## Usage Comparison + +### Before (30+ lines) + +```rust +let address_tree_pubkey = rpc.get_address_tree_v2().tree; +let state_tree_info = rpc.get_random_state_tree_info().unwrap(); + +let user_address = derive_address(&user_pda.to_bytes(), &address_tree_pubkey.to_bytes(), &program_id.to_bytes()); +let game_address = derive_address(&game_pda.to_bytes(), &address_tree_pubkey.to_bytes(), &program_id.to_bytes()); +let mint_address = derive_cmint_compressed_address(&mint_signer_pda, &address_tree_pubkey); + +let rpc_result = rpc.get_validity_proof( + vec![], + vec![ + AddressWithTree { address: user_address, tree: address_tree_pubkey }, + AddressWithTree { address: game_address, tree: address_tree_pubkey }, + AddressWithTree { address: mint_address, tree: address_tree_pubkey }, + ], + None, +).await?.value; + +let packed = pack_proof(&program_id, rpc_result.clone(), &state_tree_info, state_tree_info.cpi_context)?; + +let instruction_data = MyInstruction { + proof: rpc_result.proof, + user_address_tree_info: packed.packed_tree_infos.address_trees[0], + game_address_tree_info: packed.packed_tree_infos.address_trees[1], + mint_address_tree_info: packed.packed_tree_infos.address_trees[2], + output_state_tree_index: packed.output_tree_index, + // ... +}; + +let instruction = Instruction { + program_id, + accounts: [accounts.to_account_metas(None), packed.remaining_accounts].concat(), + data: instruction_data.data(), +}; +``` + +### After (5 lines proof setup) + +```rust +let result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::pda(user_pda), + CreateAccountsProofInput::pda(game_pda), + CreateAccountsProofInput::mint(mint_signer_pda), + ], +).await?; + +// Just pass create_accounts_proof - macros default to params.create_accounts_proof.* +let instruction_data = MyInstruction { + create_accounts_proof: result.create_accounts_proof, + // ... other app-specific params +}; + +let instruction = Instruction { + program_id, + accounts: [accounts.to_account_metas(None), result.remaining_accounts].concat(), + data: instruction_data.data(), +}; +``` + +## Design Decisions + +| Decision | Rationale | +| ------------------------------ | --------------------------------------------------------------------------------- | +| Single address tree | INIT flow always uses V2 address tree; simplifies API | +| Single `address_tree_info` | All accounts use same tree, so one info suffices; macros use this as default | +| Derivation inside helper | Removes error-prone manual derivation | +| `program_id` as default owner | Most PDAs belong to calling program; avoids redundant param per-input | +| `pda_with_owner` escape hatch | Rare case: cross-program accounts need explicit owner | +| CPI context auto-detection | When mints present, automatically includes CPI context | +| Nested result struct | `create_accounts_proof` = instruction data, `remaining_accounts` = accounts | +| Macro defaults to proof fields | `#[compressible]` and `#[light_mint]` default to `params.create_accounts_proof.*` | + +## File Location + +| File | Contents | +| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `src/create_accounts_proof.rs` | Main implementation | +| `src/lib.rs` | Re-export `get_create_accounts_proof`, `CreateAccountsProofInput`, `CreateAccountsProof`, `CreateAccountsProofResult`, `CreateAccountsProofError` | + +## Dependencies + +```rust +use light_client::indexer::{Indexer, IndexerError, AddressWithTree}; +use light_client::rpc::{Rpc, RpcError}; +use light_ctoken_sdk::compressed_token::create_compressed_mint::derive_cmint_compressed_address; +use light_compressed_account::address::derive_address; +use light_sdk::instruction::{PackedAddressTreeInfo, ValidityProof}; +use crate::pack::{pack_proof, PackError}; +``` diff --git a/sdk-libs/compressible-client/src/create_accounts_proof.rs b/sdk-libs/compressible-client/src/create_accounts_proof.rs new file mode 100644 index 0000000000..33812c89e7 --- /dev/null +++ b/sdk-libs/compressible-client/src/create_accounts_proof.rs @@ -0,0 +1,207 @@ +//! Helper for getting validity proofs for creating new compressed accounts (INIT flow). +//! +//! This module provides an opinionated helper that: +//! - Uses a single address tree (V2) for all addresses +//! - Handles address derivation internally based on input type +//! - Packs proof into remaining accounts +//! - Returns a single `address_tree_info` since all accounts use the same tree + +use light_client::indexer::{AddressWithTree, Indexer, IndexerError}; +use light_client::rpc::{Rpc, RpcError}; +use light_ctoken_sdk::compressed_token::create_compressed_mint::derive_cmint_compressed_address; +use solana_instruction::AccountMeta; +use solana_pubkey::Pubkey; +use thiserror::Error; + +use crate::pack::{pack_proof, PackError}; + +/// Error type for create accounts proof operations. +#[derive(Debug, Error)] +pub enum CreateAccountsProofError { + #[error("Inputs cannot be empty")] + EmptyInputs, + + #[error("Indexer error: {0}")] + Indexer(#[from] IndexerError), + + #[error("RPC error: {0}")] + Rpc(RpcError), + + #[error("Pack error: {0}")] + Pack(#[from] PackError), +} + +/// Input for creating new compressed accounts. +/// `program_id` from main function is used as default owner for `Pda` variant. +#[derive(Clone, Debug)] +pub enum CreateAccountsProofInput { + /// PDA owned by the calling program (uses program_id from main fn) + Pda(Pubkey), + /// PDA with explicit owner (for cross-program accounts) + PdaWithOwner { pda: Pubkey, owner: Pubkey }, + /// CMint (always uses CTOKEN_PROGRAM_ID internally) + Mint(Pubkey), +} + +impl CreateAccountsProofInput { + /// Standard PDA owned by calling program. + /// Address derived: `derive_address(&pda, &tree, &program_id)` + pub fn pda(pda: Pubkey) -> Self { + Self::Pda(pda) + } + + /// PDA with explicit owner (rare: cross-program accounts). + /// Address derived: `derive_address(&pda, &tree, &owner)` + pub fn pda_with_owner(pda: Pubkey, owner: Pubkey) -> Self { + Self::PdaWithOwner { pda, owner } + } + + /// Compressed mint (CMint). + /// Address derived: `derive_cmint_compressed_address(&mint_signer, &tree)` + pub fn mint(mint_signer: Pubkey) -> Self { + Self::Mint(mint_signer) + } + + /// Derive the compressed address. + fn derive_address(&self, address_tree: &Pubkey, program_id: &Pubkey) -> [u8; 32] { + match self { + Self::Pda(pda) => light_compressed_account::address::derive_address( + &pda.to_bytes(), + &address_tree.to_bytes(), + &program_id.to_bytes(), + ), + Self::PdaWithOwner { pda, owner } => { + light_compressed_account::address::derive_address( + &pda.to_bytes(), + &address_tree.to_bytes(), + &owner.to_bytes(), + ) + } + Self::Mint(signer) => derive_cmint_compressed_address(signer, address_tree), + } + } +} + +// Re-export from light-compressible (SBF-compatible) +pub use light_compressible::CreateAccountsProof; + +/// Result of `get_create_accounts_proof`. +pub struct CreateAccountsProofResult { + /// Proof data to include in instruction data. + pub create_accounts_proof: CreateAccountsProof, + /// Remaining accounts to append to instruction accounts. + pub remaining_accounts: Vec, +} + +/// Gets validity proof for creating new compressed accounts (INIT flow). +/// +/// Opinionated helper that: +/// - Uses a single address tree (V2) for all addresses +/// - Handles address derivation internally based on input type +/// - Packs proof into remaining accounts +/// +/// # Arguments +/// * `rpc` - RPC client implementing `Rpc + Indexer` traits +/// * `program_id` - Your program's ID (used as default owner for Pda inputs + system config) +/// * `inputs` - Vec of `CreateAccountsProofInput` describing accounts to create +/// +/// # Returns +/// `CreateAccountsProofResult` containing proof and remaining accounts. +/// +/// # Example +/// ```rust,ignore +/// let result = get_create_accounts_proof( +/// &rpc, +/// &program_id, +/// vec![ +/// CreateAccountsProofInput::pda(user_pda), +/// CreateAccountsProofInput::pda(game_pda), +/// CreateAccountsProofInput::mint(mint_signer_pda), +/// ], +/// ).await?; +/// +/// // Just pass create_accounts_proof to instruction - macros use defaults +/// let ix = Instruction { +/// program_id, +/// accounts: [my_accounts.to_account_metas(None), result.remaining_accounts].concat(), +/// data: MyInstruction { +/// create_accounts_proof: result.create_accounts_proof, +/// // ... other params +/// }.data(), +/// }; +/// ``` +pub async fn get_create_accounts_proof( + rpc: &R, + program_id: &Pubkey, + inputs: Vec, +) -> Result { + if inputs.is_empty() { + return Err(CreateAccountsProofError::EmptyInputs); + } + + // 1. Get address tree (opinionated: always V2) + let address_tree = rpc.get_address_tree_v2(); + let address_tree_pubkey = address_tree.tree; + + // 2. Derive all compressed addresses (program_id used as default owner for Pda) + let derived_addresses: Vec<[u8; 32]> = inputs + .iter() + .map(|input| input.derive_address(&address_tree_pubkey, program_id)) + .collect(); + + // 3. Build AddressWithTree for each (all use same tree) + let addresses_with_trees: Vec = derived_addresses + .iter() + .map(|&address| AddressWithTree { + address, + tree: address_tree_pubkey, + }) + .collect(); + + // 4. Get validity proof (empty hashes = INIT flow) + let validity_proof = rpc + .get_validity_proof(vec![], addresses_with_trees, None) + .await? + .value; + + // 5. Get output state tree + let state_tree_info = rpc + .get_random_state_tree_info() + .map_err(CreateAccountsProofError::Rpc)?; + + // 6. Determine CPI context + // For INIT with mints: need CPI context for cross-program invocation + let has_mints = inputs + .iter() + .any(|i| matches!(i, CreateAccountsProofInput::Mint(_))); + let cpi_context = if has_mints { + state_tree_info.cpi_context + } else { + None + }; + + // 7. Pack proof + let packed = pack_proof( + program_id, + validity_proof.clone(), + &state_tree_info, + cpi_context, + )?; + + // All addresses use the same tree, so just take the first packed info + let address_tree_info = packed + .packed_tree_infos + .address_trees + .first() + .copied() + .ok_or(CreateAccountsProofError::EmptyInputs)?; + + Ok(CreateAccountsProofResult { + create_accounts_proof: CreateAccountsProof { + proof: validity_proof.proof, + address_tree_info, + output_state_tree_index: packed.output_tree_index, + }, + remaining_accounts: packed.remaining_accounts, + }) +} diff --git a/sdk-libs/compressible-client/src/decompress_atas.rs b/sdk-libs/compressible-client/src/decompress_atas.rs new file mode 100644 index 0000000000..2bdb4cf3e1 --- /dev/null +++ b/sdk-libs/compressible-client/src/decompress_atas.rs @@ -0,0 +1,785 @@ +//! Decompress ATA-owned compressed tokens. +//! +//! This module provides client-side functionality to decompress multiple +//! ATA-owned compressed token accounts in a single instruction with one proof. +//! +//! Two API patterns are provided: +//! +//! ## High-level async API +//! - `decompress_atas`: Async, fetches state + proof internally +//! +//! ## High-performance sync API (for apps that pre-fetch state) +//! ```ignore +//! // 1. Fetch raw account interfaces (async) +//! let account = rpc.get_ata_account_interface(&mint, &owner).await?; +//! +//! // 2. Parse into token account interface (sync) +//! let parsed = parse_token_account_interface(&account)?; +//! +//! // 3. If cold, get proof and build instructions (sync) +//! if parsed.is_cold { +//! let proof = rpc.get_validity_proof(...).await?; +//! let ixs = build_decompress_atas(&[parsed], fee_payer, Some(proof))?; +//! } +//! ``` + +use light_client::indexer::{ + CompressedTokenAccount, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, + IndexerError, ValidityProofWithContext, +}; +use light_ctoken_sdk::compat::TokenData; +use light_compressed_account::compressed_account::PackedMerkleContext; +use light_ctoken_interface::{ + instructions::{ + extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, + transfer2::MultiInputTokenDataWithContext, + }, + state::{ExtensionStruct, TokenDataVersion}, +}; +use light_ctoken_sdk::{ + compat::AccountState, + compressed_token::{ + transfer2::{ + create_transfer2_instruction, Transfer2AccountsMetaConfig, Transfer2Config, + Transfer2Inputs, + }, + CTokenAccount2, + }, + ctoken::{derive_ctoken_ata, CreateAssociatedCTokenAccount}, + error::CTokenSdkError, +}; +use light_sdk::instruction::PackedAccounts; +use solana_account::Account; +use solana_instruction::Instruction; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; +use spl_token_2022::state::Account as SplTokenAccount; +use thiserror::Error; + +/// Error type for decompress ATA operations. +#[derive(Debug, Error)] +pub enum DecompressAtaError { + #[error("Indexer error: {0}")] + Indexer(#[from] IndexerError), + + #[error("CToken SDK error: {0}")] + CTokenSdk(#[from] CTokenSdkError), + + #[error("No state trees in proof")] + NoStateTreesInProof, + + #[error("Program error: {0}")] + ProgramError(#[from] ProgramError), + + #[error("Cold ATA missing compressed data at index {0}")] + MissingCompressedData(usize), + + #[error("Proof required for cold ATAs")] + ProofRequired, + + #[error("Invalid account data")] + InvalidAccountData, +} + +// ============================================================================ +// Raw Account Interface +// ============================================================================ + +/// Context for decompressing a cold ATA. +/// Contains all data needed to build decompression instructions. +#[derive(Debug, Clone)] +pub struct AtaDecompressionContext { + /// Full compressed token account from indexer. + pub compressed: CompressedTokenAccount, + /// Wallet owner (signer for decompression). + pub wallet_owner: Pubkey, + /// Token mint. + pub mint: Pubkey, + /// ATA derivation bump. + pub bump: u8, +} + +/// Raw ATA account interface - Account bytes are ALWAYS present. +/// +/// For hot accounts: actual on-chain bytes. +/// For cold accounts: synthetic SPL Token Account format bytes. +/// +/// Use `parse_token_account_interface()` to extract typed `TokenData`. +#[derive(Debug, Clone)] +pub struct AtaAccountInterface { + /// The ATA pubkey. + pub pubkey: Pubkey, + /// Raw Solana Account - always present. + /// Hot: actual on-chain bytes. + /// Cold: synthetic bytes (TokenData packed as SPL Token Account format). + pub account: Account, + /// Whether this account is compressed (needs decompression). + pub is_cold: bool, + /// Decompression context (only if cold). + pub decompression_context: Option, +} + +/// Pack TokenData into SPL Token Account format bytes (165 bytes). +pub fn pack_token_data_to_spl_bytes( + mint: &Pubkey, + owner: &Pubkey, + token_data: &TokenData, +) -> [u8; 165] { + use solana_program::program_pack::Pack; + let spl_account = SplTokenAccount { + mint: *mint, + owner: *owner, + amount: token_data.amount, + delegate: token_data.delegate.into(), + state: match token_data.state { + AccountState::Frozen => spl_token_2022::state::AccountState::Frozen, + _ => spl_token_2022::state::AccountState::Initialized, + }, + is_native: solana_program::program_option::COption::None, + delegated_amount: 0, + close_authority: solana_program::program_option::COption::None, + }; + let mut buf = [0u8; 165]; + SplTokenAccount::pack(spl_account, &mut buf).expect("pack should never fail"); + buf +} + +// ============================================================================ +// Parsed Token Account Interface +// ============================================================================ + +/// Parsed token account with decompression metadata. +/// +/// Returned by `parse_token_account_interface()`. +/// If `is_cold` is true (or `decompression_context` is Some), the account +/// needs decompression before it can be used on-chain. +#[derive(Debug, Clone)] +pub struct TokenAccountInterface { + /// Parsed token data (standard SPL-compatible type). + pub token_data: TokenData, + /// Whether this account is compressed. + pub is_cold: bool, + /// Decompression context if cold (contains all data for instruction building). + pub decompression_context: Option, +} + +impl TokenAccountInterface { + /// Convenience: get amount. + #[inline] + pub fn amount(&self) -> u64 { + self.token_data.amount + } + + /// Convenience: get delegate. + #[inline] + pub fn delegate(&self) -> Option { + self.token_data.delegate + } + + /// Convenience: get state. + #[inline] + pub fn state(&self) -> AccountState { + self.token_data.state.clone() + } + + /// Returns the compressed account hash if cold (for validity proof). + pub fn hash(&self) -> Option<[u8; 32]> { + self.decompression_context + .as_ref() + .map(|d| d.compressed.account.hash) + } +} + +/// Parse raw account interface into typed TokenAccountInterface. +/// +/// For hot accounts: unpacks SPL Token Account bytes. +/// For cold accounts: uses TokenData from decompression context. +pub fn parse_token_account_interface( + interface: &AtaAccountInterface, +) -> Result { + use solana_program::program_pack::Pack; + + if interface.is_cold { + // Cold: use TokenData from decompression context + let ctx = interface + .decompression_context + .as_ref() + .ok_or(DecompressAtaError::InvalidAccountData)?; + + Ok(TokenAccountInterface { + token_data: ctx.compressed.token.clone(), + is_cold: true, + decompression_context: Some(ctx.clone()), + }) + } else { + // Hot: unpack SPL Token Account from raw bytes + let data = &interface.account.data; + if data.len() < 165 { + return Err(DecompressAtaError::InvalidAccountData); + } + + let spl_account = SplTokenAccount::unpack(&data[..165]) + .map_err(|_| DecompressAtaError::InvalidAccountData)?; + + let token_data = TokenData { + mint: spl_account.mint, + owner: spl_account.owner, + amount: spl_account.amount, + delegate: spl_account.delegate.into(), + state: match spl_account.state { + spl_token_2022::state::AccountState::Frozen => AccountState::Frozen, + _ => AccountState::Initialized, + }, + tlv: None, + }; + + Ok(TokenAccountInterface { + token_data, + is_cold: false, + decompression_context: None, + }) + } +} + +// ============================================================================ +// Legacy AtaInterface (for backward compatibility) +// ============================================================================ + +/// Legacy decompression context. +#[derive(Debug, Clone)] +pub struct DecompressionContext { + pub compressed: CompressedTokenAccount, +} + +/// Legacy ATA interface. +/// Prefer `AtaAccountInterface` + `parse_token_account_interface()` for new code. +#[derive(Debug, Clone)] +pub struct AtaInterface { + pub ata: Pubkey, + pub owner: Pubkey, + pub mint: Pubkey, + pub bump: u8, + pub is_cold: bool, + pub token_data: TokenData, + pub raw_account: Option, + pub decompression: Option, +} + +impl AtaInterface { + #[inline] + pub fn is_cold(&self) -> bool { + self.is_cold + } + + #[inline] + pub fn is_hot(&self) -> bool { + self.raw_account.is_some() + } + + #[inline] + pub fn is_none(&self) -> bool { + !self.is_cold && self.raw_account.is_none() + } + + pub fn hash(&self) -> Option<[u8; 32]> { + self.decompression + .as_ref() + .map(|d| d.compressed.account.hash) + } + + pub fn account(&self) -> Option<&Account> { + self.raw_account.as_ref() + } + + pub fn compressed(&self) -> Option<&CompressedTokenAccount> { + self.decompression.as_ref().map(|d| &d.compressed) + } + + #[inline] + pub fn amount(&self) -> u64 { + self.token_data.amount + } + + #[inline] + pub fn delegate(&self) -> Option { + self.token_data.delegate + } + + #[inline] + pub fn state(&self) -> AccountState { + self.token_data.state.clone() + } +} + +/// Internal context for each ATA to decompress. +struct InternalAtaDecompressContext { + token_account: CompressedTokenAccount, + ata_pubkey: Pubkey, + wallet_owner: Pubkey, + ata_bump: u8, +} + +// ============================================================================ +// New API: TokenAccountInterface-based +// ============================================================================ + +/// Builds decompress instructions from parsed TokenAccountInterfaces (sync). +/// +/// High-performance API pattern: +/// 1. Fetch raw accounts: `get_ata_account_interface()` +/// 2. Parse: `parse_token_account_interface()` +/// 3. Get proof for cold accounts (async) +/// 4. Build instructions (this function, sync) +/// +/// Returns empty vec if all accounts are hot - fast exit. +/// +/// # Example +/// ```ignore +/// // 1. Fetch raw account interfaces (async) +/// let account = rpc.get_ata_account_interface(&mint, &owner).await?; +/// +/// // 2. Parse into token account interface (sync) +/// let parsed = parse_token_account_interface(&account)?; +/// +/// // 3. Collect cold hashes for proof +/// let cold_hashes: Vec<_> = [&parsed].iter() +/// .filter_map(|p| p.hash()) +/// .collect(); +/// +/// // 4. If any cold, get proof (async) +/// let proof = if cold_hashes.is_empty() { +/// None +/// } else { +/// Some(rpc.get_validity_proof(cold_hashes, vec![], None).await?.value) +/// }; +/// +/// // 5. Build instructions (sync) +/// let instructions = build_decompress_token_accounts(&[parsed], fee_payer, proof)?; +/// ``` +pub fn build_decompress_token_accounts( + token_accounts: &[TokenAccountInterface], + fee_payer: Pubkey, + validity_proof: Option, +) -> Result, DecompressAtaError> { + let mut cold_contexts: Vec = Vec::new(); + let mut create_ata_instructions = Vec::new(); + + for token_account in token_accounts.iter() { + if let Some(ctx) = &token_account.decompression_context { + // Derive ATA for destination + let (ata_pubkey, _) = derive_ctoken_ata(&ctx.wallet_owner, &ctx.mint); + + // Create ATA idempotently + let create_ata = + CreateAssociatedCTokenAccount::new(fee_payer, ctx.wallet_owner, ctx.mint) + .idempotent() + .instruction()?; + create_ata_instructions.push(create_ata); + + cold_contexts.push(InternalAtaDecompressContext { + token_account: ctx.compressed.clone(), + ata_pubkey, + wallet_owner: ctx.wallet_owner, + ata_bump: ctx.bump, + }); + } + } + + // Fast exit if all hot + if cold_contexts.is_empty() { + return Ok(vec![]); + } + + // Proof required for cold accounts + let proof = validity_proof.ok_or(DecompressAtaError::ProofRequired)?; + + // Build decompress instruction + let decompress_ix = build_batch_decompress_instruction(fee_payer, &cold_contexts, proof)?; + + let mut instructions = create_ata_instructions; + instructions.push(decompress_ix); + Ok(instructions) +} + +/// Async wrapper: decompress parsed TokenAccountInterfaces. +/// +/// Takes parsed interfaces, fetches proof internally, builds instructions. +/// Returns empty vec if all accounts are hot - fast exit. +/// +/// # Example +/// ```ignore +/// // Fetch and parse +/// let account = rpc.get_ata_account_interface(&mint, &owner).await?; +/// let parsed = parse_token_account_interface(&account)?; +/// +/// // Decompress (fetches proof internally if needed) +/// let instructions = decompress_token_accounts(&[parsed], fee_payer, &rpc).await?; +/// ``` +pub async fn decompress_token_accounts( + token_accounts: &[TokenAccountInterface], + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressAtaError> { + let cold_hashes: Vec<[u8; 32]> = token_accounts.iter().filter_map(|a| a.hash()).collect(); + + if cold_hashes.is_empty() { + return Ok(vec![]); + } + + let proof = indexer + .get_validity_proof(cold_hashes, vec![], None) + .await? + .value; + + build_decompress_token_accounts(token_accounts, fee_payer, Some(proof)) +} + +// ============================================================================ +// Legacy API: AtaInterface-based (backward compatibility) +// ============================================================================ + +/// Builds decompress instructions for ATAs synchronously (legacy API). +/// +/// Prefer `build_decompress_token_accounts` with `TokenAccountInterface` for new code. +pub fn build_decompress_atas( + atas: &[AtaInterface], + fee_payer: Pubkey, + validity_proof: Option, +) -> Result, DecompressAtaError> { + let mut cold_contexts: Vec = Vec::new(); + let mut create_ata_instructions = Vec::new(); + + for ata in atas.iter() { + if ata.is_cold { + if let Some(decompression) = &ata.decompression { + let create_ata = + CreateAssociatedCTokenAccount::new(fee_payer, ata.owner, ata.mint) + .idempotent() + .instruction()?; + create_ata_instructions.push(create_ata); + + cold_contexts.push(InternalAtaDecompressContext { + token_account: decompression.compressed.clone(), + ata_pubkey: ata.ata, + wallet_owner: ata.owner, + ata_bump: ata.bump, + }); + } + } + } + + if cold_contexts.is_empty() { + return Ok(vec![]); + } + + let proof = validity_proof.ok_or(DecompressAtaError::ProofRequired)?; + let decompress_ix = build_batch_decompress_instruction(fee_payer, &cold_contexts, proof)?; + + let mut instructions = create_ata_instructions; + instructions.push(decompress_ix); + Ok(instructions) +} + +/// Async wrapper for legacy AtaInterface API. +pub async fn decompress_atas( + atas: &[AtaInterface], + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressAtaError> { + let cold_hashes: Vec<[u8; 32]> = atas.iter().filter_map(|a| a.hash()).collect(); + + if cold_hashes.is_empty() { + return Ok(vec![]); + } + + let proof = indexer + .get_validity_proof(cold_hashes, vec![], None) + .await? + .value; + + build_decompress_atas(atas, fee_payer, Some(proof)) +} + +/// Decompresses ATA-owned compressed tokens for multiple (mint, owner) pairs. +/// +/// This is a convenience async API that fetches state and proof internally. +/// For high-performance apps, use `build_decompress_atas` with pre-fetched state. +/// +/// For each (mint, wallet_owner) pair: +/// 1. Derives the ATA address +/// 2. Fetches compressed token accounts owned by that ATA +/// 3. Gets a single validity proof for all accounts +/// 4. Creates destination ATAs if needed (idempotent) +/// 5. Builds single decompress instruction +/// +/// # Arguments +/// * `mint_owner_pairs` - List of (mint, wallet_owner) pairs to decompress +/// * `fee_payer` - Fee payer pubkey +/// * `indexer` - Indexer for fetching accounts and proofs +/// +/// # Returns +/// * Vec of instructions: [create_ata_idempotent..., decompress_all] +/// * Returns empty vec if no compressed tokens found +pub async fn decompress_atas_idempotent( + mint_owner_pairs: &[(Pubkey, Pubkey)], + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressAtaError> { + let mut create_ata_instructions = Vec::new(); + let mut all_accounts: Vec = Vec::new(); + + // Phase 1: Gather compressed token accounts and prepare ATA creation + for (mint, wallet_owner) in mint_owner_pairs { + let (ata_pubkey, ata_bump) = derive_ctoken_ata(wallet_owner, mint); + + // Query compressed tokens owned by this ATA + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new(Some( + *mint, + ))); + let result = indexer + .get_compressed_token_accounts_by_owner(&ata_pubkey, options, None) + .await?; + + let accounts = result.value.items; + if accounts.is_empty() { + continue; + } + + // Create ATA idempotently + let create_ata = CreateAssociatedCTokenAccount::new(fee_payer, *wallet_owner, *mint) + .idempotent() + .instruction()?; + create_ata_instructions.push(create_ata); + + // Collect context for each account + for acc in accounts { + all_accounts.push(InternalAtaDecompressContext { + token_account: acc, + ata_pubkey, + wallet_owner: *wallet_owner, + ata_bump, + }); + } + } + + if all_accounts.is_empty() { + return Ok(create_ata_instructions); + } + + // Phase 2: Get validity proof for all accounts + let hashes: Vec<[u8; 32]> = all_accounts + .iter() + .map(|ctx| ctx.token_account.account.hash) + .collect(); + + let proof_result = indexer.get_validity_proof(hashes, vec![], None).await?.value; + + // Phase 3: Build decompress instruction + let decompress_ix = build_batch_decompress_instruction(fee_payer, &all_accounts, proof_result)?; + + let mut instructions = create_ata_instructions; + instructions.push(decompress_ix); + Ok(instructions) +} + +fn build_batch_decompress_instruction( + fee_payer: Pubkey, + accounts: &[InternalAtaDecompressContext], + proof: ValidityProofWithContext, +) -> Result { + let mut packed_accounts = PackedAccounts::default(); + + // Pack tree infos first (inserts trees and queues) + let packed_tree_infos = proof.pack_tree_infos(&mut packed_accounts); + let tree_infos = packed_tree_infos + .state_trees + .as_ref() + .ok_or(DecompressAtaError::NoStateTreesInProof)?; + + let mut token_accounts_vec = Vec::with_capacity(accounts.len()); + let mut in_tlv_data: Vec> = Vec::with_capacity(accounts.len()); + let mut has_any_tlv = false; + + for (i, ctx) in accounts.iter().enumerate() { + let token = &ctx.token_account.token; + let tree_info = &tree_infos.packed_tree_infos[i]; + + // Insert wallet_owner as signer (for ATA, wallet signs, not ATA pubkey) + let owner_index = packed_accounts.insert_or_get_config(ctx.wallet_owner, true, false); + + // Insert ATA pubkey (as the token owner in TokenData - not a signer!) + let ata_index = packed_accounts.insert_or_get(ctx.ata_pubkey); + + // Insert mint + let mint_index = packed_accounts.insert_or_get(token.mint); + + // Insert delegate if present + let delegate_index = token + .delegate + .map(|d| packed_accounts.insert_or_get(d)) + .unwrap_or(0); + + // Insert destination ATA (same as ata_index since we decompress to the same ATA) + let destination_index = ata_index; + + // Build MultiInputTokenDataWithContext + // NOTE: prove_by_index comes from tree_info (the proof), not account (the query) + // The query may have stale prove_by_index values, but the proof is authoritative. + let source = MultiInputTokenDataWithContext { + owner: ata_index, // Token owner is ATA pubkey (not wallet!) + amount: token.amount, + has_delegate: token.delegate.is_some(), + delegate: delegate_index, + mint: mint_index, + version: TokenDataVersion::ShaFlat as u8, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: tree_info.queue_pubkey_index, + prove_by_index: tree_info.prove_by_index, + leaf_index: tree_info.leaf_index, + }, + root_index: tree_info.root_index, + }; + + // Build CTokenAccount2 for decompress + let mut ctoken_account = CTokenAccount2::new(vec![source])?; + ctoken_account.decompress_ctoken(token.amount, destination_index)?; + token_accounts_vec.push(ctoken_account); + + // Build TLV for this input (CompressedOnly extension for ATAs) + let is_frozen = token.state == AccountState::Frozen; + let tlv_vec: Vec = token + .tlv + .as_ref() + .map(|exts| { + exts.iter() + .filter_map(|ext| match ext { + ExtensionStruct::CompressedOnly(co) => { + Some(ExtensionInstructionData::CompressedOnly( + CompressedOnlyExtensionInstructionData { + delegated_amount: co.delegated_amount, + withheld_transfer_fee: co.withheld_transfer_fee, + is_frozen, + compression_index: 0, + is_ata: true, + bump: ctx.ata_bump, + owner_index, // Wallet owner who signs + }, + )) + } + _ => None, + }) + .collect() + }) + .unwrap_or_default(); + + if !tlv_vec.is_empty() { + has_any_tlv = true; + } + in_tlv_data.push(tlv_vec); + } + + // Convert packed_accounts to AccountMetas + let (packed_account_metas, _, _) = packed_accounts.to_account_metas(); + + // Build Transfer2 instruction + let meta_config = Transfer2AccountsMetaConfig::new(fee_payer, packed_account_metas); + let transfer_config = Transfer2Config::default().filter_zero_amount_outputs(); + + let inputs = Transfer2Inputs { + meta_config, + token_accounts: token_accounts_vec, + transfer_config, + validity_proof: proof.proof, + in_tlv: if has_any_tlv { Some(in_tlv_data) } else { None }, + ..Default::default() + }; + + create_transfer2_instruction(inputs).map_err(DecompressAtaError::from) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_derive_ata() { + let wallet = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let (ata, bump) = derive_ctoken_ata(&wallet, &mint); + assert_ne!(ata, wallet); + assert_ne!(ata, mint); + let _ = bump; + } + + #[test] + fn test_ata_interface_is_cold() { + let wallet = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let (ata, bump) = derive_ctoken_ata(&wallet, &mint); + + let hot_ata = AtaInterface { + ata, + owner: wallet, + mint, + bump, + is_cold: false, + token_data: TokenData { + mint, + owner: ata, + amount: 100, + delegate: None, + state: AccountState::Initialized, + tlv: None, + }, + raw_account: Some(Account::default()), + decompression: None, + }; + assert!(!hot_ata.is_cold()); + assert!(hot_ata.is_hot()); + assert_eq!(hot_ata.amount(), 100); + + let none_ata = AtaInterface { + ata, + owner: wallet, + mint, + bump, + is_cold: false, + token_data: TokenData::default(), + raw_account: None, + decompression: None, + }; + assert!(!none_ata.is_cold()); + assert!(!none_ata.is_hot()); + assert!(none_ata.is_none()); + } + + #[test] + fn test_build_decompress_atas_fast_exit() { + let wallet = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let (ata, bump) = derive_ctoken_ata(&wallet, &mint); + + // All hot - should return empty vec + let hot_atas = vec![AtaInterface { + ata, + owner: wallet, + mint, + bump, + is_cold: false, + token_data: TokenData { + mint, + owner: ata, + amount: 50, + delegate: None, + state: AccountState::Initialized, + tlv: None, + }, + raw_account: Some(Account::default()), + decompression: None, + }]; + + let result = build_decompress_atas(&hot_atas, wallet, None).unwrap(); + assert!(result.is_empty()); + } +} diff --git a/sdk-libs/compressible-client/src/decompress_mint.rs b/sdk-libs/compressible-client/src/decompress_mint.rs new file mode 100644 index 0000000000..90b145217a --- /dev/null +++ b/sdk-libs/compressible-client/src/decompress_mint.rs @@ -0,0 +1,426 @@ +//! Decompress compressed CMint accounts. +//! +//! This module provides client-side functionality to decompress compressed +//! CMint accounts (mints created via `#[compressible]` macro that have been +//! auto-compressed by forester). +//! +//! DecompressMint is permissionless - any fee_payer can decompress any +//! compressed mint. The mint_seed_pubkey is required for PDA derivation. +//! +//! Three APIs are provided: +//! - `decompress_mint`: Simple async API (fetches state + proof internally) +//! - `build_decompress_mint`: Sync, caller provides pre-fetched state + proof +//! - `decompress_cmint`: High-perf wrapper (takes MintInterface, fetches proof internally) + +use borsh::BorshDeserialize; +use light_client::indexer::{CompressedAccount, Indexer, IndexerError, ValidityProofWithContext}; +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +use light_ctoken_interface::{ + instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + state::CompressedMint, + CMINT_ADDRESS_TREE, +}; +use light_ctoken_sdk::ctoken::{derive_cmint_compressed_address, find_cmint_address, DecompressCMint}; +use solana_account::Account; +use solana_instruction::Instruction; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; +use thiserror::Error; + +/// Error type for decompress mint operations. +#[derive(Debug, Error)] +pub enum DecompressMintError { + #[error("Indexer error: {0}")] + Indexer(#[from] IndexerError), + + #[error("Compressed mint not found for signer {signer:?}")] + MintNotFound { signer: Pubkey }, + + #[error("Missing compressed mint data in account")] + MissingMintData, + + #[error("Program error: {0}")] + ProgramError(#[from] ProgramError), + + #[error("Proof required for cold mint")] + ProofRequired, +} + +/// State of a CMint - either on-chain (hot), compressed (cold), or non-existent. +#[derive(Debug, Clone)] +pub enum MintState { + /// CMint exists on-chain - no decompression needed. + Hot { account: Account }, + /// CMint is compressed - needs decompression. + Cold { + compressed: CompressedAccount, + mint_data: CompressedMint, + }, + /// CMint doesn't exist (neither on-chain nor compressed). + None, +} + +/// Interface for a CMint that provides all info needed for decompression. +/// +/// This is a superset of the solana Account type, containing: +/// - CMint pubkey (derived from signer) +/// - Signer pubkey (mint authority seed) +/// - State: Hot (on-chain), Cold (compressed), or None +#[derive(Debug, Clone)] +pub struct MintInterface { + /// The CMint PDA pubkey. + pub cmint: Pubkey, + /// The mint signer pubkey (used to derive CMint). + pub signer: Pubkey, + /// Address tree where compressed mint lives. + pub address_tree: Pubkey, + /// Compressed address (for proof). + pub compressed_address: [u8; 32], + /// Current state of the CMint. + pub state: MintState, +} + +impl MintInterface { + /// Returns true if this CMint needs decompression (is cold). + #[inline] + pub fn is_cold(&self) -> bool { + matches!(self.state, MintState::Cold { .. }) + } + + /// Returns true if this CMint exists on-chain (is hot). + #[inline] + pub fn is_hot(&self) -> bool { + matches!(self.state, MintState::Hot { .. }) + } + + /// Returns the compressed account hash if cold. + pub fn hash(&self) -> Option<[u8; 32]> { + match &self.state { + MintState::Cold { compressed, .. } => Some(compressed.hash), + _ => None, + } + } + + /// Returns the on-chain account if hot. + pub fn account(&self) -> Option<&Account> { + match &self.state { + MintState::Hot { account } => Some(account), + _ => None, + } + } + + /// Returns the compressed account and mint data if cold. + pub fn compressed(&self) -> Option<(&CompressedAccount, &CompressedMint)> { + match &self.state { + MintState::Cold { + compressed, + mint_data, + } => Some((compressed, mint_data)), + _ => None, + } + } +} + +/// Default rent payment in epochs (~24 hours per epoch) +pub const DEFAULT_RENT_PAYMENT: u8 = 2; +/// Default write top-up lamports (~3 hours rent per write) +pub const DEFAULT_WRITE_TOP_UP: u32 = 766; + +/// Builds decompress instruction for a CMint synchronously. +/// +/// This is a high-performance API for apps that pre-fetch mint state. +/// Returns empty vec if mint is hot (on-chain) - fast exit. +/// +/// # Arguments +/// * `mint` - Pre-fetched MintInterface (from `get_mint_interface`) +/// * `fee_payer` - Fee payer pubkey +/// * `validity_proof` - Proof for cold mint (required if cold, ignored if hot) +/// * `rent_payment` - Rent payment in epochs (default: 2) +/// * `write_top_up` - Lamports for future writes (default: 766) +/// +/// # Returns +/// * Vec with single decompress instruction +/// * Empty vec if mint is hot +pub fn build_decompress_mint( + mint: &MintInterface, + fee_payer: Pubkey, + validity_proof: Option, + rent_payment: Option, + write_top_up: Option, +) -> Result, DecompressMintError> { + // Fast exit if hot + let mint_data = match &mint.state { + MintState::Hot { .. } | MintState::None => return Ok(vec![]), + MintState::Cold { mint_data, .. } => mint_data, + }; + + // Check if already decompressed flag is set - return empty vec (idempotent) + if mint_data.metadata.cmint_decompressed { + return Ok(vec![]); + } + + // Proof required for cold mint + let proof_result = validity_proof.ok_or(DecompressMintError::ProofRequired)?; + + // Extract tree info from proof result + let account_info = &proof_result.accounts[0]; + let state_tree = account_info.tree_info.tree; + let input_queue = account_info.tree_info.queue; + let output_queue = account_info + .tree_info + .next_tree_info + .as_ref() + .map(|next| next.queue) + .unwrap_or(input_queue); + + // Build CompressedMintWithContext + let mint_instruction_data = CompressedMintInstructionData::try_from(mint_data.clone()) + .map_err(|_| DecompressMintError::MissingMintData)?; + + let compressed_mint_with_context = CompressedMintWithContext { + leaf_index: account_info.leaf_index as u32, + prove_by_index: account_info.root_index.proof_by_index(), + root_index: account_info.root_index.root_index().unwrap_or_default(), + address: mint.compressed_address, + mint: Some(mint_instruction_data), + }; + + // Build DecompressCMint instruction + let decompress = DecompressCMint { + mint_seed_pubkey: mint.signer, + payer: fee_payer, + authority: fee_payer, // Permissionless - any signer works + state_tree, + input_queue, + output_queue, + compressed_mint_with_context, + proof: ValidityProof(proof_result.proof.into()), + rent_payment: rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT), + write_top_up: write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP), + }; + + let ix = decompress + .instruction() + .map_err(DecompressMintError::from)?; + Ok(vec![ix]) +} + +/// High-performance wrapper: decompress pre-fetched mint. +/// +/// Takes pre-fetched `MintInterface`, fetches proof internally, builds instruction. +/// Returns empty vec if mint is hot (on-chain) - fast exit. +/// +/// # Example +/// ```ignore +/// // Pre-fetch mint state +/// let mint = rpc.get_mint_interface(&signer).await?; +/// +/// // Decompress if cold (fetches proof internally) +/// let instructions = decompress_cmint(&mint, fee_payer, &rpc).await?; +/// ``` +pub async fn decompress_cmint( + mint: &MintInterface, + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressMintError> { + // Fast exit if hot or doesn't exist + let hash = match mint.hash() { + Some(h) => h, + None => return Ok(vec![]), + }; + + // Check decompressed flag before fetching proof + if let Some((_, mint_data)) = mint.compressed() { + if mint_data.metadata.cmint_decompressed { + return Ok(vec![]); + } + } + + // Get validity proof + let proof = indexer + .get_validity_proof(vec![hash], vec![], None) + .await? + .value; + + // Build instruction (sync) + build_decompress_mint(mint, fee_payer, Some(proof), None, None) +} + +/// Request to decompress a compressed CMint. +#[derive(Debug, Clone)] +pub struct DecompressMintRequest { + /// The seed pubkey used to derive the CMint PDA. + /// This is the same value passed as `mint_signer` when the mint was created. + pub mint_seed_pubkey: Pubkey, + /// Address tree where the compressed mint was created. + /// If None, uses the default cmint address tree. + pub address_tree: Option, + /// Rent payment in epochs (must be 0 or >= 2). Default: 2 + pub rent_payment: Option, + /// Lamports for future write operations. Default: 766 + pub write_top_up: Option, +} + +impl DecompressMintRequest { + pub fn new(mint_seed_pubkey: Pubkey) -> Self { + Self { + mint_seed_pubkey, + address_tree: None, + rent_payment: None, + write_top_up: None, + } + } + + pub fn with_address_tree(mut self, address_tree: Pubkey) -> Self { + self.address_tree = Some(address_tree); + self + } + + pub fn with_rent_payment(mut self, rent_payment: u8) -> Self { + self.rent_payment = Some(rent_payment); + self + } + + pub fn with_write_top_up(mut self, write_top_up: u32) -> Self { + self.write_top_up = Some(write_top_up); + self + } +} + +/// Decompress a compressed mint with default parameters. +/// Returns empty vec if already decompressed (idempotent). +pub async fn decompress_mint( + mint_seed_pubkey: Pubkey, + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressMintError> { + decompress_mint_idempotent( + DecompressMintRequest::new(mint_seed_pubkey), + fee_payer, + indexer, + ) + .await +} + +/// Decompresses a compressed CMint to an on-chain CMint Solana account. +/// +/// This is permissionless - any fee_payer can decompress any compressed mint. +/// Returns empty vec if already decompressed (idempotent). +pub async fn decompress_mint_idempotent( + request: DecompressMintRequest, + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressMintError> { + // 1. Derive addresses + let address_tree = request + .address_tree + .unwrap_or(Pubkey::new_from_array(CMINT_ADDRESS_TREE)); + let compressed_address = + derive_cmint_compressed_address(&request.mint_seed_pubkey, &address_tree); + + // 2. Fetch compressed mint account from indexer + let compressed_account = indexer + .get_compressed_account(compressed_address, None) + .await? + .value + .ok_or(DecompressMintError::MintNotFound { + signer: request.mint_seed_pubkey, + })?; + + // 3. Check if data is empty (already decompressed - empty shell remains) + // After decompression, the compressed account has empty data but the address persists. + let data = match compressed_account.data.as_ref() { + Some(d) if !d.data.is_empty() => d, + _ => return Ok(vec![]), // Empty data = already decompressed (idempotent) + }; + + // 4. Parse mint data from compressed account + let mint_data = CompressedMint::try_from_slice(&data.data) + .map_err(|_| DecompressMintError::MissingMintData)?; + + // 5. Check if already decompressed flag is set - return empty vec (idempotent) + if mint_data.metadata.cmint_decompressed { + return Ok(vec![]); + } + + // 5. Get validity proof + let proof_result = indexer + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await? + .value; + + // 6. Extract tree info from proof result + let account_info = &proof_result.accounts[0]; + let state_tree = account_info.tree_info.tree; + let input_queue = account_info.tree_info.queue; + let output_queue = account_info + .tree_info + .next_tree_info + .as_ref() + .map(|next| next.queue) + .unwrap_or(input_queue); + + // 7. Build CompressedMintWithContext + // NOTE: prove_by_index and leaf_index come from account_info (the proof), not compressed_account + // The query may have stale values, but the proof is authoritative. + let mint_instruction_data = CompressedMintInstructionData::try_from(mint_data) + .map_err(|_| DecompressMintError::MissingMintData)?; + + let compressed_mint_with_context = CompressedMintWithContext { + leaf_index: account_info.leaf_index as u32, + prove_by_index: account_info.root_index.proof_by_index(), + root_index: account_info.root_index.root_index().unwrap_or_default(), + address: compressed_address, + mint: Some(mint_instruction_data), + }; + + // 8. Build DecompressCMint instruction + let decompress = DecompressCMint { + mint_seed_pubkey: request.mint_seed_pubkey, + payer: fee_payer, + authority: fee_payer, // Permissionless - any signer works + state_tree, + input_queue, + output_queue, + compressed_mint_with_context, + proof: ValidityProof(proof_result.proof.into()), + rent_payment: request.rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT), + write_top_up: request.write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP), + }; + + let ix = decompress + .instruction() + .map_err(DecompressMintError::from)?; + Ok(vec![ix]) +} + +/// Derive MintInterface from signer pubkey and on-chain/compressed state. +/// Helper for creating MintInterface when you have the data. +pub fn create_mint_interface( + signer: Pubkey, + address_tree: Pubkey, + onchain_account: Option, + compressed: Option<(CompressedAccount, CompressedMint)>, +) -> MintInterface { + let (cmint, _) = find_cmint_address(&signer); + let compressed_address = derive_cmint_compressed_address(&signer, &address_tree); + + let state = if let Some(account) = onchain_account { + MintState::Hot { account } + } else if let Some((compressed, mint_data)) = compressed { + MintState::Cold { + compressed, + mint_data, + } + } else { + MintState::None + }; + + MintInterface { + cmint, + signer, + address_tree, + compressed_address, + state, + } +} diff --git a/sdk-libs/compressible-client/src/initialize_config.rs b/sdk-libs/compressible-client/src/initialize_config.rs new file mode 100644 index 0000000000..15915c8cac --- /dev/null +++ b/sdk-libs/compressible-client/src/initialize_config.rs @@ -0,0 +1,164 @@ +//! Helper for initializing compression config with sensible defaults. + +#[cfg(feature = "anchor")] +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +use light_sdk::compressible::config::CompressibleConfig; +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; + +/// Default address tree v2 pubkey. +pub const ADDRESS_TREE_V2: Pubkey = + solana_pubkey::pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"); + +/// Default write top-up value (5000 lamports). +pub const DEFAULT_INIT_WRITE_TOP_UP: u32 = 5_000; + +/// Instruction data format matching anchor-generated `initialize_compression_config`. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct InitializeCompressionConfigAnchorData { + pub write_top_up: u32, + pub rent_sponsor: Pubkey, + pub compression_authority: Pubkey, + pub rent_config: light_compressible::rent::RentConfig, + pub address_space: Vec, +} + +/// Builder for creating `initialize_compression_config` instruction with sensible defaults. +/// +/// Uses: +/// - Address tree v2 (`amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx`) +/// - Default rent config +/// - Default write top-up (5000 lamports) +/// +/// # Example +/// ```ignore +/// let (instruction, config_pda) = InitializeRentFreeConfig::new( +/// &program_id, +/// &fee_payer, +/// &program_data_pda, +/// rent_sponsor_pubkey, +/// compression_authority_pubkey, +/// ).build(); +/// ``` +pub struct InitializeRentFreeConfig { + program_id: Pubkey, + fee_payer: Pubkey, + program_data_pda: Pubkey, + authority: Option, + rent_sponsor: Pubkey, + compression_authority: Pubkey, + rent_config: light_compressible::rent::RentConfig, + write_top_up: u32, + address_space: Vec, + config_bump: u8, +} + +impl InitializeRentFreeConfig { + /// Creates a new builder with required fields and default values. + /// + /// # Arguments + /// * `program_id` - The program that owns the compression config + /// * `fee_payer` - The account paying for the transaction + /// * `program_data_pda` - The program data PDA (BPF upgradeable loader) + /// * `rent_sponsor` - The rent sponsor pubkey + /// * `compression_authority` - The compression authority pubkey + pub fn new( + program_id: &Pubkey, + fee_payer: &Pubkey, + program_data_pda: &Pubkey, + rent_sponsor: Pubkey, + compression_authority: Pubkey, + ) -> Self { + Self { + program_id: *program_id, + fee_payer: *fee_payer, + program_data_pda: *program_data_pda, + authority: None, + rent_sponsor, + compression_authority, + rent_config: light_compressible::rent::RentConfig::default(), + write_top_up: DEFAULT_INIT_WRITE_TOP_UP, + address_space: vec![ADDRESS_TREE_V2], + config_bump: 0, + } + } + + /// Sets the authority signer (defaults to fee_payer if not set). + pub fn authority(mut self, authority: Pubkey) -> Self { + self.authority = Some(authority); + self + } + + /// Overrides the default rent config. + pub fn rent_config(mut self, rent_config: light_compressible::rent::RentConfig) -> Self { + self.rent_config = rent_config; + self + } + + /// Overrides the default write top-up value. + pub fn write_top_up(mut self, write_top_up: u32) -> Self { + self.write_top_up = write_top_up; + self + } + + /// Overrides the default address space (address tree v2). + pub fn address_space(mut self, address_space: Vec) -> Self { + self.address_space = address_space; + self + } + + /// Sets the config bump (default 0). + pub fn config_bump(mut self, config_bump: u8) -> Self { + self.config_bump = config_bump; + self + } + + /// Builds the instruction and returns (instruction, config_pda). + /// + /// The returned instruction is ready to send with Anchor's generated discriminator. + pub fn build(self) -> (Instruction, Pubkey) { + let authority = self.authority.unwrap_or(self.fee_payer); + let (config_pda, _) = CompressibleConfig::derive_pda(&self.program_id, self.config_bump); + + let accounts = vec![ + AccountMeta::new(self.fee_payer, true), // payer + AccountMeta::new(config_pda, false), // config + AccountMeta::new_readonly(self.program_data_pda, false), // program_data + AccountMeta::new_readonly(authority, true), // authority + AccountMeta::new_readonly( + solana_pubkey::pubkey!("11111111111111111111111111111111"), + false, + ), // system_program + ]; + + let instruction_data = InitializeCompressionConfigAnchorData { + write_top_up: self.write_top_up, + rent_sponsor: self.rent_sponsor, + compression_authority: self.compression_authority, + rent_config: self.rent_config, + address_space: self.address_space, + }; + + // Anchor discriminator for "initialize_compression_config" + // SHA256("global:initialize_compression_config")[..8] + const DISCRIMINATOR: [u8; 8] = [133, 228, 12, 169, 56, 76, 222, 61]; + + let serialized_data = instruction_data + .try_to_vec() + .expect("Failed to serialize instruction data"); + + let mut data = Vec::with_capacity(DISCRIMINATOR.len() + serialized_data.len()); + data.extend_from_slice(&DISCRIMINATOR); + data.extend_from_slice(&serialized_data); + + let instruction = Instruction { + program_id: self.program_id, + accounts, + data, + }; + + (instruction, config_pda) + } +} diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs index f621868df0..d073aa2c12 100644 --- a/sdk-libs/compressible-client/src/lib.rs +++ b/sdk-libs/compressible-client/src/lib.rs @@ -1,10 +1,51 @@ +pub mod create_accounts_proof; +pub mod decompress_atas; +pub mod decompress_mint; pub mod get_compressible_account; +pub mod initialize_config; +pub mod pack; + +pub use create_accounts_proof::{ + get_create_accounts_proof, CreateAccountsProofError, CreateAccountsProofInput, + CreateAccountsProofResult, +}; +// Re-export from light-compressible for convenience (SBF-compatible definition) +pub use decompress_atas::{ + // Legacy API (backward compatible) + build_decompress_atas, + // New API (recommended) + build_decompress_token_accounts, + decompress_atas, + decompress_atas_idempotent, + decompress_token_accounts, + pack_token_data_to_spl_bytes, + parse_token_account_interface, + AtaAccountInterface, + AtaDecompressionContext, + AtaInterface, + DecompressAtaError, + DecompressionContext, + TokenAccountInterface, +}; +pub use light_compressible::CreateAccountsProof; +// Re-export TokenData for convenience (standard SPL-compatible type) +pub use decompress_mint::{ + build_decompress_mint, create_mint_interface, decompress_cmint, decompress_mint, + decompress_mint_idempotent, DecompressMintError, DecompressMintRequest, MintInterface, + MintState, DEFAULT_RENT_PAYMENT, DEFAULT_WRITE_TOP_UP, +}; +pub use initialize_config::InitializeRentFreeConfig; +pub use light_ctoken_sdk::compat::TokenData; +pub use pack::{pack_proof, PackError, PackedProofResult}; #[cfg(feature = "anchor")] use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; use light_client::indexer::{CompressedAccount, TreeInfo, ValidityProofWithContext}; +use light_ctoken_sdk::ctoken::{ + COMPRESSIBLE_CONFIG_V1, CTOKEN_CPI_AUTHORITY, CTOKEN_PROGRAM_ID, RENT_SPONSOR, +}; pub use light_sdk::compressible::config::CompressibleConfig; use light_sdk::{ compressible::{compression_info::CompressedAccountData, Pack}, @@ -58,10 +99,178 @@ pub struct CompressAccountsIdempotentData { pub system_accounts_offset: u8, } +/// Account interface for unified hot/cold account handling. +/// Represents an account that may be on-chain (hot) or compressed (cold). +#[derive(Clone, Debug)] +pub struct AccountInterface { + /// The account's public key (PDA address) + pub pubkey: Pubkey, + /// True if the account is compressed (cold), false if on-chain (hot) + pub is_cold: bool, + /// Context needed for decompression (only present when is_cold is true) + pub decompression_context: Option, +} + +/// Context needed to decompress a compressed PDA account. +#[derive(Clone, Debug)] +pub struct PdaDecompressionContext { + /// The compressed account data from indexer + pub compressed_account: CompressedAccount, +} + +impl AccountInterface { + /// Create a new cold (compressed) account interface + pub fn cold(pubkey: Pubkey, compressed_account: CompressedAccount) -> Self { + Self { + pubkey, + is_cold: true, + decompression_context: Some(PdaDecompressionContext { compressed_account }), + } + } + + /// Create a new hot (on-chain) account interface + pub fn hot(pubkey: Pubkey) -> Self { + Self { + pubkey, + is_cold: false, + decompression_context: None, + } + } + + /// Get the compressed account data bytes if available + pub fn compressed_data(&self) -> Option<&[u8]> { + self.decompression_context + .as_ref() + .and_then(|ctx| ctx.compressed_account.data.as_ref()) + .map(|d| d.data.as_slice()) + } +} + +/// A rent-free decompression request combining account interface and variant. +/// Generic over V (the CompressedAccountVariant type from the program). +#[derive(Clone, Debug)] +pub struct RentFreeDecompressAccount { + /// The account interface (contains pubkey and cold/hot state) + pub account_interface: AccountInterface, + /// The typed variant (e.g., CompressedAccountVariant::UserRecord { ... }) + pub variant: V, +} + +impl RentFreeDecompressAccount { + /// Create a new decompression request + pub fn new(account_interface: AccountInterface, variant: V) -> Self { + Self { + account_interface, + variant, + } + } + + /// Create decompression request from account interface and seeds. + /// + /// The seeds type determines which variant constructor to call. + /// Data is extracted from interface, passed to `IntoVariant::into_variant()`. + /// + /// # Arguments + /// * `interface` - The account interface (must be cold/compressed) + /// * `seeds` - Seeds struct (e.g., `UserRecordSeeds`) that implements `IntoVariant` + /// + /// # Example + /// ```ignore + /// RentFreeDecompressAccount::from_seeds( + /// AccountInterface::cold(user_record_pda, compressed_user), + /// UserRecordSeeds { authority, mint_authority, owner, category_id }, + /// )? + /// ``` + #[cfg(feature = "anchor")] + pub fn from_seeds( + interface: AccountInterface, + seeds: S, + ) -> Result + where + S: light_sdk::compressible::IntoVariant, + { + let data = interface.compressed_data().ok_or_else(|| { + anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::AccountNotInitialized) + })?; + let variant = seeds.into_variant(data)?; + Ok(Self::new(interface, variant)) + } + + /// Create decompression request for CToken account. + /// + /// Parses TokenData from interface.compressed_data() internally. + /// The CToken variant type determines how to wrap into the full variant. + /// + /// # Arguments + /// * `interface` - The account interface (must be cold/compressed) + /// * `ctoken_variant` - CToken variant (e.g., `CTokenAccountVariant::Vault { cmint }`) + /// + /// # Example + /// ```ignore + /// RentFreeDecompressAccount::from_ctoken( + /// AccountInterface::cold(vault_pda, compressed_vault.account), + /// CTokenAccountVariant::Vault { cmint: cmint_pda }, + /// )? + /// ``` + #[cfg(feature = "anchor")] + pub fn from_ctoken( + interface: AccountInterface, + ctoken_variant: T, + ) -> Result + where + T: light_sdk::compressible::IntoCTokenVariant, + { + use anchor_lang::AnchorDeserialize; + + let data = interface.compressed_data().ok_or_else(|| { + anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::AccountNotInitialized) + })?; + let token_data = TokenData::deserialize(&mut &data[..])?; + let variant = ctoken_variant.into_ctoken_variant(token_data); + Ok(Self::new(interface, variant)) + } +} + /// Instruction builders for compressible accounts pub mod compressible_instruction { use super::*; + /// Helpers for decompress_accounts_idempotent instruction + pub mod decompress { + use super::*; + + /// Returns program account metas for decompress_accounts_idempotent with CToken support. + /// Includes ctoken_rent_sponsor, ctoken_program, ctoken_cpi_authority, ctoken_config. + pub fn accounts( + fee_payer: Pubkey, + config: Pubkey, + rent_sponsor: Pubkey, + ) -> Vec { + vec![ + AccountMeta::new(fee_payer, true), + AccountMeta::new_readonly(config, false), + AccountMeta::new(rent_sponsor, false), + AccountMeta::new(RENT_SPONSOR, false), + AccountMeta::new_readonly(CTOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(CTOKEN_CPI_AUTHORITY, false), + AccountMeta::new_readonly(COMPRESSIBLE_CONFIG_V1, false), + ] + } + + /// Returns program account metas for PDA-only decompression (no CToken accounts). + pub fn accounts_pda_only( + fee_payer: Pubkey, + config: Pubkey, + rent_sponsor: Pubkey, + ) -> Vec { + vec![ + AccountMeta::new(fee_payer, true), + AccountMeta::new_readonly(config, false), + AccountMeta::new(rent_sponsor, false), + ] + } + } + /// SHA256("global:initialize_compression_config")[..8] pub const INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = [133, 228, 12, 169, 56, 76, 222, 61]; @@ -163,9 +372,9 @@ pub mod compressible_instruction { } } - /// Builds decompress_accounts_idempotent instruction + /// Builds decompress_accounts_idempotent instruction (raw version with explicit discriminator) #[allow(clippy::too_many_arguments)] - pub fn decompress_accounts_idempotent( + pub fn build_decompress_idempotent_raw( program_id: &Pubkey, discriminator: &[u8], decompressed_account_addresses: &[Pubkey], @@ -202,13 +411,18 @@ pub mod compressible_instruction { } // pack cpi_context_account if required. + // CRITICAL: When both PDAs and tokens exist, tokens execute LAST (consuming the CPI context). + // CPI context validation checks: cpi_context.associated_tree == first_input_of_executor.tree + // So we must use the FIRST TOKEN's cpi_context, not the first PDA's. if has_pdas && has_tokens { - let cpi_context_of_first_input = - compressed_accounts[0].0.tree_info.cpi_context.unwrap(); - let system_config = SystemAccountMetaConfig::new_with_cpi_context( - *program_id, - cpi_context_of_first_input, - ); + // Find the first token account's CPI context + let first_token_cpi_context = compressed_accounts + .iter() + .find(|(acc, _)| acc.owner == C_TOKEN_PROGRAM_ID.into()) + .map(|(acc, _)| acc.tree_info.cpi_context.unwrap()) + .expect("has_tokens is true so there must be a token"); + let system_config = + SystemAccountMetaConfig::new_with_cpi_context(*program_id, first_token_cpi_context); remaining_accounts.add_system_accounts_v2(system_config)?; } else { let system_config = SystemAccountMetaConfig::new(*program_id); @@ -234,19 +448,19 @@ pub mod compressible_instruction { let mut typed_compressed_accounts = Vec::with_capacity(compressed_accounts.len()); - for (compressed_account, data) in compressed_accounts { - let queue_index = remaining_accounts.insert_or_get(compressed_account.tree_info.queue); + // The compressed_accounts are expected to be in the SAME ORDER as the + // validity_proof_with_context.accounts. This is because both are derived + // from the same hash order passed to get_validity_proof(). + // We use index-based matching instead of queue+leaf_index to handle + // accounts on different trees with potentially colliding indices. + for (i, (compressed_account, data)) in compressed_accounts.iter().enumerate() { + // Insert the queue for this account (needed for the packed context) + let _queue_index = remaining_accounts.insert_or_get(compressed_account.tree_info.queue); - let tree_info = packed_tree_infos_slice - .iter() - .find(|pti| { - pti.queue_pubkey_index == queue_index - && pti.leaf_index == compressed_account.leaf_index - }) - .copied() - .ok_or( - "Matching PackedStateTreeInfo (queue_pubkey_index + leaf_index) not found", - )?; + // Use index-based matching - the i-th compressed account uses the i-th tree info + let tree_info = packed_tree_infos_slice.get(i).copied().ok_or( + "Tree info index out of bounds - compressed_accounts length must match validity proof accounts length", + )?; let packed_data = data.pack(&mut remaining_accounts); typed_compressed_accounts.push(CompressedAccountData { @@ -356,4 +570,63 @@ pub mod compressible_instruction { data, }) } + + /// Builds decompress_accounts_idempotent instruction from RentFreeDecompressAccount items. + /// Automatically filters cold accounts and returns None if no accounts need decompression. + /// + /// # Arguments + /// * `program_id` - The program ID + /// * `accounts` - Vec of RentFreeDecompressAccount (cold accounts will be decompressed, hot skipped) + /// * `program_account_metas` - Account metas from generated .to_account_metas(None) + /// * `validity_proof_with_context` - The validity proof for the cold accounts + #[allow(clippy::too_many_arguments)] + pub fn build_decompress_idempotent( + program_id: &Pubkey, + accounts: Vec>, + program_account_metas: Vec, + validity_proof_with_context: ValidityProofWithContext, + ) -> Result, Box> + where + V: Pack + Clone + std::fmt::Debug, + { + // Filter to only cold accounts + let cold_accounts: Vec<_> = accounts + .into_iter() + .filter(|a| a.account_interface.is_cold) + .collect(); + + if cold_accounts.is_empty() { + return Ok(None); + } + + // Extract pubkeys and (CompressedAccount, variant) pairs + let decompressed_account_addresses: Vec = cold_accounts + .iter() + .map(|a| a.account_interface.pubkey) + .collect(); + + let compressed_accounts: Vec<(CompressedAccount, V)> = cold_accounts + .into_iter() + .map(|a| { + let compressed_account = a + .account_interface + .decompression_context + .expect("Cold account must have decompression context") + .compressed_account; + (compressed_account, a.variant) + }) + .collect(); + + // Build instruction using raw function + let instruction = build_decompress_idempotent_raw( + program_id, + &DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &decompressed_account_addresses, + &compressed_accounts, + &program_account_metas, + validity_proof_with_context, + )?; + + Ok(Some(instruction)) + } } diff --git a/sdk-libs/compressible-client/src/pack.rs b/sdk-libs/compressible-client/src/pack.rs new file mode 100644 index 0000000000..ac3fffdeab --- /dev/null +++ b/sdk-libs/compressible-client/src/pack.rs @@ -0,0 +1,119 @@ +//! Helper for packing validity proofs into remaining accounts. +//! +//! # Usage +//! +//! ```rust,ignore +//! // 1. Derive addresses & get proof +//! let proof = rpc.get_validity_proof(hashes, addresses, None).await?.value; +//! +//! // 2. Pack into remaining accounts +//! let packed = pack_proof(&program_id, proof.clone(), &output_tree, cpi_context)?; +//! +//! // 3. Build instruction +//! let ix = Instruction { +//! program_id, +//! accounts: [my_accounts.to_account_metas(None), packed.remaining_accounts].concat(), +//! data: MyInstruction { +//! proof: proof.proof, +//! address_tree_infos: packed.packed_tree_infos.address_trees, +//! output_tree_index: packed.output_tree_index, +//! }.data(), +//! }; +//! ``` + +use light_client::indexer::{TreeInfo, ValidityProofWithContext}; +use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; +use solana_instruction::AccountMeta; +use solana_pubkey::Pubkey; +use thiserror::Error; + +pub use light_sdk::instruction::{PackedAddressTreeInfo, PackedStateTreeInfo}; + +#[derive(Debug, Error)] +pub enum PackError { + #[error("Failed to add system accounts: {0}")] + SystemAccounts(#[from] light_sdk::error::LightSdkError), +} + +/// Packed state tree infos from validity proof. +#[derive(Clone, Default, Debug)] +pub struct PackedStateTreeInfos { + pub packed_tree_infos: Vec, + pub output_tree_index: u8, +} + +/// Packed tree infos from validity proof. +#[derive(Clone, Default, Debug)] +pub struct PackedTreeInfos { + pub state_trees: Option, + pub address_trees: Vec, +} + +/// Result of packing a validity proof into remaining accounts. +pub struct PackedProofResult { + /// Remaining accounts to append to your instruction's accounts. + pub remaining_accounts: Vec, + /// Packed tree infos from the proof. Use `.address_trees` or `.state_trees` as needed. + pub packed_tree_infos: PackedTreeInfos, + /// Index of output tree in remaining accounts. Pass to instruction data. + pub output_tree_index: u8, + /// Offset where system accounts start. Pass to instruction data if needed. + pub system_accounts_offset: u8, +} + +/// Packs a validity proof into remaining accounts for instruction building. +/// +/// Handles all the `PackedAccounts` boilerplate: +/// - Adds system accounts (with optional CPI context) +/// - Inserts output tree queue +/// - Packs tree infos from proof +/// +/// # Arguments +/// - `program_id`: Your program's ID +/// - `proof`: Validity proof from `get_validity_proof()` +/// - `output_tree`: Tree info for writing outputs (from `get_random_state_tree_info()`) +/// - `cpi_context`: CPI context pubkey. Required when mixing PDAs with tokens in same tx. +/// Get from `tree_info.cpi_context`. +/// +/// # Returns +/// `PackedProofResult` containing remaining accounts and indices for instruction data. +pub fn pack_proof( + program_id: &Pubkey, + proof: ValidityProofWithContext, + output_tree: &TreeInfo, + cpi_context: Option, +) -> Result { + let mut packed = PackedAccounts::default(); + + let system_config = match cpi_context { + Some(ctx) => SystemAccountMetaConfig::new_with_cpi_context(*program_id, ctx), + None => SystemAccountMetaConfig::new(*program_id), + }; + packed.add_system_accounts_v2(system_config)?; + + let output_queue = output_tree + .next_tree_info + .as_ref() + .map(|n| n.queue) + .unwrap_or(output_tree.queue); + let output_tree_index = packed.insert_or_get(output_queue); + + let client_packed_tree_infos = proof.pack_tree_infos(&mut packed); + let (remaining_accounts, system_offset, _) = packed.to_account_metas(); + + // Convert from light_client's types to our local types + let packed_tree_infos = PackedTreeInfos { + state_trees: client_packed_tree_infos.state_trees.map(|st| PackedStateTreeInfos { + packed_tree_infos: st.packed_tree_infos, + output_tree_index: st.output_tree_index, + }), + address_trees: client_packed_tree_infos.address_trees, + }; + + Ok(PackedProofResult { + remaining_accounts, + packed_tree_infos, + output_tree_index, + system_accounts_offset: system_offset as u8, + }) +} diff --git a/sdk-libs/compressible-client/wrapper.md b/sdk-libs/compressible-client/wrapper.md new file mode 100644 index 0000000000..a9f21ffc02 --- /dev/null +++ b/sdk-libs/compressible-client/wrapper.md @@ -0,0 +1,1013 @@ +# Unified Decompression Wrapper Specification + +## Problem Statement + +Clients need a single entry point to decompress mixed account types: + +- **ATAs** (Associated Token Accounts) - compression_only tokens owned by ATA pubkey +- **Program-owned CTokens** - tokens owned by program PDAs +- **Program-owned PDAs** - compressed program state + +Each type has different invocation patterns: +| Type | Invocation | Signer | Program Required | +|------|-----------|--------|------------------| +| ATA | Direct invoke to ctoken | wallet_owner | No | +| Program CToken | CPI from user program | program PDA | Yes | +| Program PDA | CPI from user program | program PDA | Yes | + +## Decision Tree + +``` + ┌──────────────────────────────────────┐ + │ What type of compressed account? │ + └───────────────────┬──────────────────┘ + │ + ┌───────────────────────────────┼───────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌───────────────┐ ┌─────────────────┐ ┌──────────────────┐ + │ ATA │ │ Program PDA │ │ Program CToken │ + │ (wallet owns │ │ (program owns │ │ (program PDA │ + │ the tokens) │ │ the state) │ │ owns tokens) │ + └───────┬───────┘ └────────┬────────┘ └────────┬─────────┘ + │ │ │ + ▼ └──────────────┬───────────────┘ + ┌───────────────┐ │ + │ SDK-ONLY │ ▼ + │ │ ┌───────────────────────┐ + │ decompress_ │ │ REQUIRES PROGRAM │ + │ atas_ │ │ │ + │ idempotent() │ │ User must have on- │ + │ │ │ chain program with │ + │ - No program │ │ decompress_accounts_ │ + │ deployment │ │ idempotent handler │ + │ - Wallet │ │ │ + │ signs │ │ - Program CPI │ + │ - Direct │ │ - Program signs │ + │ invoke │ │ - Needs T: Pack type │ + └───────────────┘ └───────────────────────┘ +``` + +## System Architecture + +``` + ┌─────────────────────────────────┐ + │ decompress_all() │ + │ Unified Entry Point │ + └───────────────┬─────────────────┘ + │ + ┌───────────────┼───────────────┐ + ▼ ▼ ▼ + ┌──────────┐ ┌──────────────┐ ┌──────────┐ + │ ATAs │ │ Program PDAs │ │ Program │ + │ │ │ │ │ CTokens │ + └────┬─────┘ └──────┬───────┘ └────┬─────┘ + │ │ │ + ▼ └───────┬────────┘ + ┌─────────────────┐ │ + │ decompress_atas │ ▼ + │ _idempotent() │ ┌─────────────────────────┐ + │ │ │ decompress_accounts │ + │ Direct invoke │ │ _idempotent() │ + │ to ctoken │ │ │ + └────────┬────────┘ │ CPI through user │ + │ │ program (requires │ + │ │ program_id + discrim) │ + ▼ └────────────┬────────────┘ + ┌─────────────────┐ │ + │ Transaction 1 │ ▼ + │ (SDK-only) │ ┌─────────────────────────┐ + │ │ │ Transaction 2 │ + │ create_ata... │ │ (User Program CPI) │ + │ decompress_ata │ │ │ + └─────────────────┘ │ decompress_pdas+tokens │ + └─────────────────────────┘ +``` + +## Data Flow + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Client Input │ +│ DecompressRequest { │ +│ kind: AccountKind, // ATA | ProgramPda | ProgramCtoken │ +│ pubkey: Pubkey, // The account identifier │ +│ hash: Option<[u8;32]> // Optional: specific compressed hash │ +│ } │ +└─────────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────┐ +│ Phase 1: Classification │ +│ │ +│ for request in requests: │ +│ match request.kind { │ +│ ATA { wallet_owner, mint } => ata_requests.push(...) │ +│ ProgramPda { program_id, seeds } => pda_requests.push(...) │ +│ ProgramCtoken { program_id, seeds } => ctoken_requests.push(...) │ +│ } │ +└─────────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────┐ +│ Phase 2: Query Indexer │ +│ │ +│ ATAs: │ +│ indexer.get_compressed_token_accounts_by_owner(ata_pubkey, mint) │ +│ │ +│ Program PDAs: │ +│ indexer.get_compressed_account_by_address(derived_address) │ +│ │ +│ Program CTokens: │ +│ indexer.get_compressed_token_accounts_by_owner(pda_pubkey, mint) │ +└─────────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────┐ +│ Phase 3: Proof Generation │ +│ │ +│ ATA hashes -> get_validity_proof() -> ata_proof │ +│ PDA + CToken hashes -> get_validity_proof() -> program_proof │ +│ │ +│ Note: PDAs and CTokens share a proof because they're batched in │ +│ the same CPI call through the user program │ +└─────────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────┐ +│ Phase 4: Instruction Building │ +│ │ +│ let mut instructions = Vec::new(); │ +│ │ +│ // ATAs: SDK-only, no program involvement │ +│ if !ata_requests.is_empty() { │ +│ instructions.extend(decompress_atas_idempotent(...)?); │ +│ } │ +│ │ +│ // Program accounts: requires CPI through user program │ +│ if !pda_requests.is_empty() || !ctoken_requests.is_empty() { │ +│ let ix = decompress_accounts_idempotent( │ +│ program_id, │ +│ discriminator, // User provides this │ +│ ... │ +│ )?; │ +│ instructions.push(ix); │ +│ } │ +└─────────────────────────────────┬────────────────────────────────────────┘ + │ + ▼ +┌──────────────────────────────────────────────────────────────────────────┐ +│ Output │ +│ │ +│ DecompressResult { │ +│ ata_instructions: Vec, // Can be sent standalone │ +│ program_instructions: Vec, // Requires user program │ +│ } │ +│ │ +│ OR │ +│ │ +│ Vec where each batch can be a single tx │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +## API Design + +### Account Kind Enum + +```rust +/// Identifies the type of compressed account for decompression +#[derive(Debug, Clone)] +pub enum AccountKind { + /// ATA-owned compressed token (compression_only) + /// Owner is the ATA pubkey derived from wallet_owner + mint + /// Decompression is SDK-only (direct invoke to ctoken program) + Ata { + wallet_owner: Pubkey, + mint: Pubkey, + }, + + /// Program-owned compressed PDA + /// Requires CPI through user's program + ProgramPda { + /// The program that owns this account + program_id: Pubkey, + /// The PDA pubkey (destination for decompression) + pda_pubkey: Pubkey, + }, + + /// Program-owned compressed token + /// Owner is a PDA of user's program + /// Requires CPI through user's program + ProgramCtoken { + /// The program that owns the PDA which owns the ctoken + program_id: Pubkey, + /// The PDA pubkey that owns the compressed token + owner_pda: Pubkey, + /// The token mint + mint: Pubkey, + }, +} +``` + +### Request Structure + +```rust +/// A request to decompress a specific compressed account +#[derive(Debug, Clone)] +pub struct DecompressRequest { + /// The kind of account and its identifying information + pub kind: AccountKind, + + /// Optional: specific compressed account hash(es) to decompress + /// If None, decompresses ALL compressed accounts matching the kind + pub hashes: Option>, +} +``` + +### Program Config (for PDA/CToken operations) + +```rust +/// Configuration for program-owned account decompression +/// Only needed if decompressing ProgramPda or ProgramCtoken accounts +#[derive(Debug, Clone)] +pub struct ProgramDecompressConfig { + /// The program ID that owns the accounts + pub program_id: Pubkey, + + /// The discriminator for decompress_accounts_idempotent instruction + /// SHA256("global:decompress_accounts_idempotent")[..8] + pub discriminator: [u8; 8], + + /// Account metas for the program's DecompressAccountsIdempotent accounts struct + /// This is program-specific and must be provided by the client + pub program_account_metas: Vec, + + /// Packed account data type deserializer + /// Used to convert compressed account data to the program's variant type + pub pack_fn: fn(&CompressedAccount) -> Result, +} +``` + +### Result Structure + +```rust +/// Result of decompress_all operation +#[derive(Debug)] +pub struct DecompressResult { + /// Instructions for ATA decompression (SDK-only, no program needed) + /// These can be sent as a standalone transaction + pub ata_instructions: Vec, + + /// Instructions for program-owned account decompression + /// These require the user's program to be deployed + /// Each inner Vec is a set of instructions that must go in the same tx + pub program_instructions: Vec>, + + /// Accounts that were skipped (already decompressed or not found) + pub skipped: Vec, +} + +#[derive(Debug)] +pub struct SkippedAccount { + pub kind: AccountKind, + pub reason: SkipReason, +} + +#[derive(Debug)] +pub enum SkipReason { + NotFound, + AlreadyDecompressed, + InvalidState, +} +``` + +### Main Entry Point + +````rust +/// Unified decompression entry point +/// +/// Given a list of accounts to decompress (with their kinds), generates +/// the appropriate instructions to decompress them. +/// +/// # Arguments +/// * `requests` - List of decompression requests +/// * `fee_payer` - The fee payer for all transactions +/// * `program_config` - Required if any ProgramPda or ProgramCtoken requests exist +/// * `indexer` - Indexer for querying compressed state and proofs +/// +/// # Returns +/// * `DecompressResult` containing categorized instructions +/// +/// # Example +/// ```rust +/// let requests = vec![ +/// DecompressRequest { +/// kind: AccountKind::Ata { wallet_owner, mint }, +/// hashes: None, // decompress all +/// }, +/// DecompressRequest { +/// kind: AccountKind::ProgramPda { program_id, pda_pubkey }, +/// hashes: Some(vec![specific_hash]), +/// }, +/// ]; +/// +/// let result = decompress_all( +/// &requests, +/// fee_payer, +/// Some(program_config), // needed for ProgramPda +/// &indexer, +/// ).await?; +/// +/// // Send ATA instructions first (no dependencies) +/// rpc.send_transaction(result.ata_instructions); +/// +/// // Send program instructions (requires user program) +/// for ix_batch in result.program_instructions { +/// rpc.send_transaction(ix_batch); +/// } +/// ``` +pub async fn decompress_all( + requests: &[DecompressRequest], + fee_payer: Pubkey, + program_config: Option<&ProgramDecompressConfig>, + indexer: &I, +) -> Result +```` + +### Convenience Functions + +```rust +/// Decompress only ATAs (simplified API for common case) +pub async fn decompress_only_atas( + wallet_mints: &[(Pubkey, Pubkey)], // (wallet_owner, mint) pairs + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressError> { + let requests: Vec<_> = wallet_mints + .iter() + .map(|(wallet_owner, mint)| DecompressRequest { + kind: AccountKind::Ata { + wallet_owner: *wallet_owner, + mint: *mint, + }, + hashes: None, + }) + .collect(); + + let result = decompress_all(&requests, fee_payer, None, indexer).await?; + Ok(result.ata_instructions) +} + +/// Decompress program accounts with a pre-built config +pub async fn decompress_program_accounts( + program_id: &Pubkey, + discriminator: &[u8; 8], + pda_pubkeys: &[Pubkey], + program_account_metas: Vec, + fee_payer: Pubkey, + indexer: &I, +) -> Result +``` + +## Implementation Plan + +### Phase 1: Core Types (wrapper_types.rs) + +1. `AccountKind` enum +2. `DecompressRequest` struct +3. `ProgramDecompressConfig` struct +4. `DecompressResult` struct +5. `DecompressError` error enum + +### Phase 2: Request Classification (wrapper.rs) + +1. `classify_requests()` - separates ATAs from program accounts +2. `validate_program_config()` - ensures config exists when needed + +### Phase 3: Indexer Queries + +1. Batch ATA queries by wallet_owner +2. Batch PDA queries by program_id +3. Batch CToken queries by owner_pda + +### Phase 4: Proof Generation + +1. Collect all hashes per category +2. `get_validity_proof()` for ATA hashes +3. `get_validity_proof()` for program account hashes + +### Phase 5: Instruction Building + +1. Call existing `decompress_atas_idempotent()` for ATAs +2. Call existing `decompress_accounts_idempotent()` for program accounts + +## Transaction Batching Considerations + +``` +┌────────────────────────────────────────────────────────────────────────┐ +│ Transaction Batching Rules │ +│ │ +│ 1. ATAs: All in same tx (or split by compute limit) │ +│ - create_ata_idempotent... (multiple) │ +│ - decompress_batch (single ix, multiple inputs) │ +│ │ +│ 2. Program PDAs + CTokens: All in same tx if same program │ +│ - decompress_accounts_idempotent(pda1, pda2, ctoken1, ctoken2) │ +│ - Order: PDAs first, then CTokens (CPI context handling) │ +│ │ +│ 3. Mixed programs: Separate transactions │ +│ - Each program_id gets its own decompress_accounts_idempotent │ +│ │ +│ 4. Compute limits: May need to split large batches │ +│ - ~200k CU per decompression │ +│ - ~1.4M CU limit per tx │ +│ - Max ~7 decompressions per tx │ +└────────────────────────────────────────────────────────────────────────┘ +``` + +## Error Handling + +```rust +#[derive(Debug, Error)] +pub enum DecompressError { + #[error("Indexer error: {0}")] + Indexer(#[from] IndexerError), + + #[error("CToken SDK error: {0}")] + CTokenSdk(#[from] CTokenSdkError), + + #[error("Program config required for ProgramPda or ProgramCtoken accounts")] + ProgramConfigRequired, + + #[error("Program ID mismatch: expected {expected}, got {got}")] + ProgramIdMismatch { expected: Pubkey, got: Pubkey }, + + #[error("No compressed accounts found for any request")] + NoAccountsFound, + + #[error("Instruction building failed: {0}")] + InstructionBuild(String), +} +``` + +## Usage Examples + +### Example 1: Decompress User's ATAs Only + +```rust +// User wants to decompress all their compressed USDC and SOL tokens +let result = decompress_all( + &[ + DecompressRequest { + kind: AccountKind::Ata { + wallet_owner: user_wallet, + mint: usdc_mint + }, + hashes: None, + }, + DecompressRequest { + kind: AccountKind::Ata { + wallet_owner: user_wallet, + mint: wsol_mint + }, + hashes: None, + }, + ], + user_wallet, // fee payer + None, // no program config needed + &indexer, +).await?; + +// Send single transaction +send_transaction(result.ata_instructions).await?; +``` + +### Example 2: Decompress Game State (Mixed) + +```rust +// Game has: user PDA (score), reward tokens (program-owned) +let program_config = ProgramDecompressConfig { + program_id: game_program_id, + discriminator: game::instruction::DecompressAccountsIdempotent::DISCRIMINATOR, + program_account_metas: game::accounts::DecompressAccountsIdempotent { + fee_payer: user_wallet, + config: config_pda, + rent_sponsor: rent_sponsor, + // ... other accounts + }.to_account_metas(None), + pack_fn: |acc| GameAccountVariant::try_from(acc), +}; + +let result = decompress_all( + &[ + // User's ATA (their own tokens) + DecompressRequest { + kind: AccountKind::Ata { + wallet_owner: user_wallet, + mint: reward_mint + }, + hashes: None, + }, + // Game PDA (score state) + DecompressRequest { + kind: AccountKind::ProgramPda { + program_id: game_program_id, + pda_pubkey: score_pda, + }, + hashes: None, + }, + // Program-owned reward tokens + DecompressRequest { + kind: AccountKind::ProgramCtoken { + program_id: game_program_id, + owner_pda: reward_vault_pda, + mint: reward_mint, + }, + hashes: None, + }, + ], + user_wallet, + Some(&program_config), + &indexer, +).await?; + +// Transaction 1: ATAs (no program needed) +send_transaction(result.ata_instructions).await?; + +// Transaction 2: Game state + program tokens (needs game program) +for ix_batch in result.program_instructions { + send_transaction(ix_batch).await?; +} +``` + +## Key Constraints + +1. **ATAs are SDK-only**: No program deployment needed, wallet signs directly +2. **Program accounts need CPI**: User must have deployed program with `decompress_accounts_idempotent` +3. **Proof batching**: Single proof for multiple accounts of same category +4. **CPI context ordering**: When mixing PDAs + CTokens, PDAs write first, CTokens consume last +5. **Program ID grouping**: Different programs = different transactions + +## Files to Create/Modify + +1. `sdk-libs/compressible-client/src/wrapper_types.rs` - Type definitions +2. `sdk-libs/compressible-client/src/wrapper.rs` - Main implementation +3. `sdk-libs/compressible-client/src/lib.rs` - Export new modules + +## Dependencies + +- Existing: `decompress_atas_idempotent` (just implemented) +- Existing: `compressible_instruction::decompress_accounts_idempotent` +- Existing: `light_client::indexer::Indexer` trait +- Existing: `light_sdk::compressible::Pack` trait + +--- + +## Design Alternative: Simpler Approach + +The main complexity in the unified wrapper comes from handling the generic `T: Pack` constraint for program-owned accounts. Each program defines its own `CompressedAccountVariant` enum. + +### Alternative: Two-Tier API + +Instead of one function that does everything, provide: + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Two-Tier API │ +│ │ +│ Tier 1: SDK-Only (no user program needed) │ +│ ───────────────────────────────────────── │ +│ decompress_atas_idempotent() - Already implemented │ +│ │ +│ Tier 2: Program-Aware (requires user program types) │ +│ ─────────────────────────────────────────────────── │ +│ decompress_program_accounts() - Generic over program variant │ +│ │ +│ Combination Helper (convenience, not generic) │ +│ ───────────────────────────────────────────── │ +│ DecompressBuilder - Builder pattern for combining requests │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Builder Pattern API + +```rust +/// Builder for constructing mixed decompression requests +pub struct DecompressBuilder<'a, I: Indexer> { + indexer: &'a I, + fee_payer: Pubkey, + ata_requests: Vec, + /// Program requests stored as pre-built instructions (caller handles T: Pack) + program_instructions: Vec, +} + +impl<'a, I: Indexer> DecompressBuilder<'a, I> { + pub fn new(indexer: &'a I, fee_payer: Pubkey) -> Self { + Self { + indexer, + fee_payer, + ata_requests: Vec::new(), + program_instructions: Vec::new(), + } + } + + /// Add an ATA to decompress + pub fn add_ata(mut self, wallet_owner: Pubkey, mint: Pubkey) -> Self { + self.ata_requests.push(DecompressAtaRequest { + wallet_owner, + mint, + hashes: None, + }); + self + } + + /// Add multiple ATAs for same wallet + pub fn add_atas(mut self, wallet_owner: Pubkey, mints: &[Pubkey]) -> Self { + for mint in mints { + self.ata_requests.push(DecompressAtaRequest { + wallet_owner, + mint: *mint, + hashes: None, + }); + } + self + } + + /// Add a pre-built program decompression instruction + /// Caller is responsible for building this with correct T: Pack type + pub fn add_program_instruction(mut self, instruction: Instruction) -> Self { + self.program_instructions.push(instruction); + self + } + + /// Build all instructions + pub async fn build(self) -> Result { + let ata_instructions = if self.ata_requests.is_empty() { + Vec::new() + } else { + decompress_atas_idempotent(&self.ata_requests, self.fee_payer, self.indexer).await? + }; + + Ok(DecompressResult { + ata_instructions, + program_instructions: self.program_instructions, + skipped: Vec::new(), + }) + } +} +``` + +### Usage with Builder + +```rust +// Simple: ATAs only +let result = DecompressBuilder::new(&indexer, fee_payer) + .add_ata(wallet, usdc_mint) + .add_ata(wallet, wsol_mint) + .build() + .await?; + +// Mixed: ATAs + program accounts +// Step 1: User builds their program instruction (they know the types) +let program_ix = compressible_instruction::decompress_accounts_idempotent::( + &game_program_id, + &discriminator, + &[pda1, pda2], + &[(compressed_pda1, variant1), (compressed_pda2, variant2)], + &program_account_metas, + validity_proof, +)?; + +// Step 2: Combine with builder +let result = DecompressBuilder::new(&indexer, fee_payer) + .add_ata(wallet, reward_mint) + .add_program_instruction(program_ix) + .build() + .await?; +``` + +### Why This is Better + +1. **No phantom type complexity**: Caller handles `T: Pack` themselves +2. **No trait objects/dynamic dispatch**: Instructions are concrete +3. **Composable**: Easy to add more instruction types later +4. **Type safe**: Program-specific types stay in caller's code +5. **Simpler implementation**: Builder just aggregates, doesn't transform + +--- + +## Recommended Implementation + +Given the complexity of generic type handling across programs, the recommended implementation is: + +### Core Functions (Already Exist / Just Built) + +1. `decompress_atas_idempotent()` - SDK-only ATA decompression +2. `compressible_instruction::decompress_accounts_idempotent()` - Program account decompression + +### New Functions to Add + +```rust +/// Convenience: Decompress all ATAs for a wallet across multiple mints +pub async fn decompress_wallet_atas( + wallet_owner: Pubkey, + mints: &[Pubkey], + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressAtaError> { + let requests: Vec<_> = mints + .iter() + .map(|mint| DecompressAtaRequest { + wallet_owner, + mint: *mint, + hashes: None, + }) + .collect(); + decompress_atas_idempotent(&requests, fee_payer, indexer).await +} + +/// Query helper: Find all compressed accounts for program-owned PDAs +/// Returns data needed to call decompress_accounts_idempotent +pub async fn query_program_compressed_accounts( + program_id: &Pubkey, + pda_pubkeys: &[Pubkey], + indexer: &I, +) -> Result { + // Derives addresses, queries indexer, returns compressed accounts + // Caller then deserializes data into their T type +} + +/// Query helper: Find all compressed tokens owned by program PDAs +pub async fn query_program_compressed_tokens( + owner_pdas: &[Pubkey], + mint: Option, + indexer: &I, +) -> Result, DecompressError> { + // Queries indexer for each owner PDA +} +``` + +### Final Recommended API + +``` +┌──────────────────────────────────────────────────────────────────────────┐ +│ Recommended API Surface │ +│ │ +│ HIGH-LEVEL (SDK-only) │ +│ ───────────────────── │ +│ decompress_atas_idempotent() // Multiple ATAs, one proof │ +│ decompress_wallet_atas() // All ATAs for wallet │ +│ decompress_all_for_ata() // Single ATA convenience │ +│ │ +│ QUERY HELPERS (for program accounts) │ +│ ───────────────────────────────────── │ +│ query_program_compressed_accounts() // Find PDAs, return raw data │ +│ query_program_compressed_tokens() // Find program-owned tokens │ +│ │ +│ LOW-LEVEL (generic, caller provides types) │ +│ ────────────────────────────────────────── │ +│ decompress_accounts_idempotent() // Build instruction │ +│ │ +│ BUILDER (composition) │ +│ ───────────────────── │ +│ DecompressBuilder // Combine ATAs + program ixs │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +This keeps the API clean while acknowledging that program-specific type handling must stay with the caller. + +--- + +## Complete End-to-End Flow Diagrams + +### Scenario 1: User Decompresses Their Own ATAs (SDK-Only) + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ User: "I want to decompress my USDC and SOL tokens" │ +└──────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Client Code │ +│ │ +│ let requests = vec![ │ +│ DecompressAtaRequest { wallet_owner, mint: usdc_mint, hashes: None }, │ +│ DecompressAtaRequest { wallet_owner, mint: wsol_mint, hashes: None }, │ +│ ]; │ +│ │ +│ let instructions = decompress_atas_idempotent(&requests, wallet, &ix) │ +│ .await?; │ +└──────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Internal: decompress_atas_idempotent │ +│ │ +│ 1. Derive ATA pubkeys: (wallet, usdc) -> ata1, (wallet, sol) -> ata2 │ +│ │ +│ 2. Query indexer: │ +│ indexer.get_compressed_token_accounts_by_owner(ata1, usdc) │ +│ indexer.get_compressed_token_accounts_by_owner(ata2, sol) │ +│ │ +│ 3. Get single proof for all hashes: │ +│ indexer.get_validity_proof([hash1, hash2, hash3...]) │ +│ │ +│ 4. Build instructions: │ +│ - create_ata_idempotent(wallet, usdc) │ +│ - create_ata_idempotent(wallet, sol) │ +│ - transfer2(decompress all tokens to ATAs) │ +└──────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Single Transaction │ +│ │ +│ Instructions: [create_ata_usdc, create_ata_sol, decompress_batch] │ +│ Signers: [wallet] │ +│ │ +│ No on-chain program needed - direct invoke to ctoken program │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Scenario 2: Game Decompresses Player State (Program Required) + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ User: "I want to decompress my game score PDA and reward tokens" │ +└──────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Client Code (Game-Specific) │ +│ │ +│ // Step 1: Query compressed accounts │ +│ let score_address = derive_address(&score_pda, &tree, &game_id); │ +│ let score_account = indexer.get_compressed_account(score_address).await?; │ +│ let token_accounts = indexer │ +│ .get_compressed_token_accounts_by_owner(&reward_vault_pda, mint) │ +│ .await?; │ +│ │ +│ // Step 2: Deserialize into game's variant types │ +│ let score_variant = GameVariant::Score( │ +│ ScoreData::deserialize(&score_account.data)? │ +│ ); │ +│ let token_variants: Vec<_> = token_accounts.iter() │ +│ .map(|acc| GameVariant::Token(acc.token.clone())) │ +│ .collect(); │ +│ │ +│ // Step 3: Get proof for all accounts │ +│ let all_hashes = [score_account.hash].iter() │ +│ .chain(token_accounts.iter().map(|a| a.account.hash)) │ +│ .collect(); │ +│ let proof = indexer.get_validity_proof(all_hashes, [], None).await?; │ +│ │ +│ // Step 4: Build program instruction │ +│ let ix = decompress_accounts_idempotent::( │ +│ &game_program_id, │ +│ &game::DECOMPRESS_DISCRIMINATOR, │ +│ &[score_pda, reward_vault_pda], │ +│ &[(score_account, score_variant), ...token_variants], │ +│ &game_account_metas, │ +│ proof, │ +│ )?; │ +└──────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Single Transaction │ +│ │ +│ Instructions: [decompress_accounts_idempotent] │ +│ Signers: [wallet] │ +│ │ +│ On-chain game program executes: │ +│ 1. CPI to light-system-program (writes PDA to CPI context) │ +│ 2. CPI to ctoken-program (decompresses tokens, consumes CPI context) │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### Scenario 3: Mixed ATAs + Program Accounts + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ User: "Decompress my personal tokens AND my game state" │ +└──────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Client Code (Using Builder Pattern) │ +│ │ +│ // Build ATAs │ +│ let ata_ixs = decompress_atas_idempotent(&[ │ +│ DecompressAtaRequest { wallet_owner, mint: personal_token, ... }, │ +│ ], wallet, &indexer).await?; │ +│ │ +│ // Build program instruction (separate, with game types) │ +│ let game_ix = build_game_decompress_ix(...)?; // as shown in Scenario 2 │ +│ │ +│ // Combine using builder │ +│ let result = DecompressBuilder::new(&indexer, wallet) │ +│ .with_ata_instructions(ata_ixs) │ +│ .with_program_instruction(game_ix) │ +│ .build()?; │ +└──────────────────────────────────┬──────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Two Transactions (or combined if compute allows) │ +│ │ +│ Transaction 1 (ATAs - SDK only): │ +│ - create_ata_idempotent │ +│ - decompress_atas_batch │ +│ - Signers: [wallet] │ +│ │ +│ Transaction 2 (Game - needs program): │ +│ - decompress_accounts_idempotent │ +│ - Signers: [wallet] │ +│ - Program: game_program handles CPI │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Implementation Checklist + +### Already Implemented + +- [x] `decompress_atas_idempotent` - SDK-only ATA decompression +- [x] `decompress_all_for_ata` - Single ATA convenience +- [x] `decompress_multiple_atas` - Simple multi-ATA wrapper +- [x] `compressible_instruction::decompress_accounts_idempotent` - Program accounts + +### To Implement (New Wrapper Layer) + +1. **Query Helpers** (new file: `query_helpers.rs`) + +```rust +// Find compressed PDAs by their derived addresses +pub async fn query_compressed_pdas( + pda_pubkeys: &[Pubkey], + address_tree: &Pubkey, + program_id: &Pubkey, + indexer: &I, +) -> Result, DecompressError>; + +// Find compressed tokens by owner PDAs +pub async fn query_compressed_tokens_by_owners( + owner_pdas: &[Pubkey], + mint: Option, + indexer: &I, +) -> Result)>, DecompressError>; +``` + +2. **DecompressBuilder** (new file: `decompress_builder.rs`) + +```rust +pub struct DecompressBuilder { ... } + +impl DecompressBuilder { + pub fn new(fee_payer: Pubkey) -> Self; + pub fn with_ata_instructions(self, ixs: Vec) -> Self; + pub fn with_program_instruction(self, ix: Instruction) -> Self; + pub fn build(self) -> DecompressResult; +} +``` + +3. **Additional Convenience Functions** (add to `decompress_atas.rs`) + +```rust +// Decompress all ATAs for a wallet +pub async fn decompress_wallet_atas( + wallet: Pubkey, + mints: &[Pubkey], + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressAtaError>; +``` + +### Files to Create/Modify + +| File | Action | Contents | +| --------------------------- | ------ | ----------------------------------- | +| `src/query_helpers.rs` | Create | Query utilities for PDAs and tokens | +| `src/decompress_builder.rs` | Create | Builder for combining requests | +| `src/decompress_atas.rs` | Modify | Add `decompress_wallet_atas` | +| `src/lib.rs` | Modify | Export new modules | + +--- + +## Summary + +The recommended approach is a **two-tier API**: + +1. **SDK-only tier** (for ATAs): Fully automatic, no program needed +2. **Program-aware tier**: Caller provides types, we provide query helpers + +The builder pattern bridges both tiers for mixed use cases, keeping type safety while maintaining flexibility. + +Key insight: We cannot fully abstract away the `T: Pack` generic without either: + +- Runtime type erasure (losing type safety) +- Macro-generated code per program (complexity) + +The builder pattern sidesteps this by letting callers handle their own types while we handle the orchestration. diff --git a/sdk-libs/macros/MACRO-NEW.md b/sdk-libs/macros/MACRO-NEW.md new file mode 100644 index 0000000000..ee00070b3f --- /dev/null +++ b/sdk-libs/macros/MACRO-NEW.md @@ -0,0 +1,792 @@ +# Compressible Macro - Final Implementation + +## Overview + +The `#[compressible(...)]` macro generates all types and code needed for rent-free account compression/decompression. This document shows exactly what exists now and how to use it. + +## Status: Complete (Phase 1-8) + +All phases implemented and tested, including Phase 8 CToken seed refactor. See `csdk-anchor-full-derived-test` for working example. + +### Phase 8 Key Changes + +- `CTokenAccountVariant` now has struct variants with Pubkey fields for seeds +- `PackedCTokenAccountVariant` has struct variants with u8 idx fields +- `CTokenSeedProvider` trait no longer requires accounts struct parameter +- `DecompressAccountsIdempotent` no longer needs named seed accounts +- All seed resolution happens via variant idx fields and `post_system_accounts` + +--- + +## 1. Macro Declaration + +```rust +#[compressible( + // PDA accounts with seeds + UserRecord = (seeds = ("user_record", ctx.authority, ctx.mint_authority, data.owner, data.category_id.to_le_bytes())), + GameSession = (seeds = (GAME_SESSION_SEED, ctx.user, ctx.authority, data.session_id.to_le_bytes())), + + // Token accounts (CTokens) + Vault = (is_token, seeds = ("vault", ctx.cmint), authority = ("vault_authority")), + + // Instruction data fields (for data.* seeds) + owner = Pubkey, + category_id = u64, + session_id = u64, +)] +pub mod my_program { ... } +``` + +### Seed Types + +- `ctx.*` - Context accounts (Pubkeys from instruction accounts) +- `data.*` - Data fields (from compressed account data, verified at construction time) +- String literals - Static seeds +- Constants - e.g., `GAME_SESSION_SEED` + +--- + +## 2. Generated Types + +### 2.1 CompressedAccountVariant Enum (Struct Variants) + +```rust +pub enum CompressedAccountVariant { + // Unpacked variants (with ctx.* Pubkeys) + UserRecord { + data: UserRecord, + authority: Pubkey, + mint_authority: Pubkey, + }, + GameSession { + data: GameSession, + user: Pubkey, + authority: Pubkey, + }, + + // Packed variants (with u8 indices into remaining_accounts) + PackedUserRecord { + data: PackedUserRecord, + authority_idx: u8, + mint_authority_idx: u8, + }, + PackedGameSession { + data: PackedGameSession, + user_idx: u8, + authority_idx: u8, + }, + + // CToken variant (unchanged) + CTokenData(CTokenData), +} +``` + +### 2.2 Seeds Structs (All Seeds - ctx._ + data._) + +```rust +pub struct UserRecordSeeds { + pub authority: Pubkey, // ctx.authority + pub mint_authority: Pubkey, // ctx.mint_authority + pub owner: Pubkey, // data.owner (verified against account) + pub category_id: u64, // data.category_id (verified against account) +} + +pub struct GameSessionSeeds { + pub user: Pubkey, // ctx.user + pub authority: Pubkey, // ctx.authority + pub session_id: u64, // data.session_id (verified against account) +} +``` + +### 2.3 Constructor Methods + +```rust +impl CompressedAccountVariant { + /// Deserializes data and verifies data.* seeds match. + pub fn user_record( + account_data: &[u8], + seeds: UserRecordSeeds, + ) -> Result { + let data = UserRecord::deserialize(&mut &account_data[..])?; + + // Verify data.* seeds match actual compressed data + if data.owner != seeds.owner { return Err(SeedMismatch); } + if data.category_id != seeds.category_id { return Err(SeedMismatch); } + + Ok(Self::UserRecord { + data, + authority: seeds.authority, + mint_authority: seeds.mint_authority, + }) + } + + pub fn game_session( + account_data: &[u8], + seeds: GameSessionSeeds, + ) -> Result { ... } +} +``` + +### 2.4 SeedParams Removed + +`SeedParams` is no longer needed in instruction data. All seeds are now resolved: + +- `ctx.*` seeds: From variant idx fields → resolved on-chain via `post_system_accounts` +- `data.*` seeds: From unpacked compressed account data (`self.field`) + +--- + +## 3. Client API Types + +### 3.1 AccountInterface + +```rust +pub struct AccountInterface { + pub pubkey: Pubkey, + pub is_cold: bool, + pub decompression_context: Option, +} + +pub struct PdaDecompressionContext { + pub compressed_account: CompressedAccount, +} + +impl AccountInterface { + pub fn cold(pubkey: Pubkey, compressed_account: CompressedAccount) -> Self; + pub fn hot(pubkey: Pubkey) -> Self; + pub fn compressed_data(&self) -> Option<&[u8]>; +} +``` + +### 3.2 RentFreeDecompressAccount + +```rust +pub struct RentFreeDecompressAccount { + pub account_interface: AccountInterface, + pub variant: V, +} +``` + +### 3.3 Instruction Builders + +```rust +// Existing API (still works) +pub fn decompress_accounts_idempotent( + program_id: &Pubkey, + discriminator: &[u8], + decompressed_account_addresses: &[Pubkey], + compressed_accounts: &[(CompressedAccount, T)], + program_account_metas: &[AccountMeta], + validity_proof_with_context: ValidityProofWithContext, +) -> Result + +// New API (filters cold accounts automatically) +pub fn decompress_accounts_idempotent_new( + program_id: &Pubkey, + accounts: Vec>, + program_account_metas: &[AccountMeta], + validity_proof_with_context: ValidityProofWithContext, + discriminator: Option<&[u8]>, +) -> Result, Error> // Returns None if no cold accounts +``` + +--- + +## 4. Client Usage (Complete Example) + +From `csdk-anchor-full-derived-test/tests/basic_test.rs`: + +```rust +use csdk_anchor_full_derived_test::{CTokenAccountVariant, CompressedAccountVariant}; +use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ + GameSessionSeeds, UserRecordSeeds, // NO SeedParams needed +}; +use light_compressible_client::{ + compressible_instruction, AccountInterface, RentFreeDecompressAccount, +}; + +// 1. Fetch compressed accounts from indexer +let compressed_user = rpc + .get_compressed_account(user_compressed_address, None) + .await? + .value + .unwrap(); + +let compressed_game = rpc + .get_compressed_account(game_compressed_address, None) + .await? + .value + .unwrap(); + +// 2. Fetch compressed token accounts +let compressed_vault_accounts = rpc + .get_compressed_token_accounts_by_owner(&vault_pda, None, None) + .await? + .value + .items; +let compressed_vault = &compressed_vault_accounts[0]; + +// 3. Get validity proof for all accounts +let rpc_result = rpc + .get_validity_proof( + vec![ + compressed_user.hash, + compressed_game.hash, + compressed_vault.account.hash, + ], + vec![], + None, + ) + .await? + .value; + +// 4. Create AccountInterface for each cold account (from RPC response) +let user_interface = AccountInterface::cold(user_record_pda, compressed_user.clone()); +let game_interface = AccountInterface::cold(game_session_pda, compressed_game.clone()); +let vault_interface = AccountInterface::cold(vault_pda, compressed_vault.account.clone()); + +// 5. Construct variants using generated constructors (verifies data.* seeds match) +let user_variant = CompressedAccountVariant::user_record( + user_interface.compressed_data().unwrap(), + UserRecordSeeds { + authority: authority.pubkey(), + mint_authority: mint_authority.pubkey(), + owner, // Must match compressed data + category_id, // Must match compressed data + }, +).expect("UserRecord seed verification failed"); + +let game_variant = CompressedAccountVariant::game_session( + game_interface.compressed_data().unwrap(), + GameSessionSeeds { + user: payer.pubkey(), + authority: authority.pubkey(), + session_id, // Must match compressed data + }, +).expect("GameSession seed verification failed"); + +let vault_ctoken_data = light_ctoken_sdk::compat::CTokenData { + variant: CTokenAccountVariant::Vault, + token_data: compressed_vault.token.clone(), +}; + +// 6. Build RentFreeDecompressAccount for each account +let decompress_accounts = vec![ + RentFreeDecompressAccount::new(user_interface, user_variant), + RentFreeDecompressAccount::new(game_interface, game_variant), + RentFreeDecompressAccount::new( + vault_interface, + CompressedAccountVariant::CTokenData(vault_ctoken_data), + ), +]; + +// 7. Build decompress instruction using NEW API - NO SeedParams or seed accounts needed! +let decompress_instruction = compressible_instruction::decompress_accounts_idempotent_new( + &program_id, + decompress_accounts, + compressible_instruction::decompress::accounts(payer.pubkey(), config_pda, payer.pubkey()), + rpc_result, +)? +.expect("Should have cold accounts to decompress"); + +// 8. Send transaction - done! +rpc.create_and_send_transaction(&[decompress_instruction], &payer.pubkey(), &[&payer]) + .await?; +``` + +--- + +## 5. On-Chain Flow + +### 5.1 What Happens When Instruction Executes + +``` +1. UNPACK + PackedUserRecord { data, authority_idx: 3, mint_authority_idx: 5 } + -> authority = remaining_accounts[3].key + -> mint_authority = remaining_accounts[5].key + -> UserRecord { data, authority, mint_authority } + +2. DERIVE PDA + UserRecordCtxSeeds { authority, mint_authority } + + + self (unpacked UserRecord) + -> seeds = ["user_record", authority, mint_authority, self.owner, self.category_id.to_le_bytes()] + -> derived_pda = Pubkey::find_program_address(&seeds, program_id) + +3. VERIFY + assert!(derived_pda == target_solana_account.key) + +4. CREATE/WRITE + if !account_exists { + create_pda(derived_pda) + } + write_data(data) +``` + +### 5.2 CPI Context Batching (Mixed PDAs + Tokens) + +``` +CRITICAL: When has_pdas && has_tokens: +1. PDAs FIRST: LightSystemProgramCpi.write_to_cpi_context_first() +2. Tokens LAST: invoke() with cpi_context (consumes context) + +Client must use FIRST TOKEN's cpi_context when packing. +``` + +--- + +## 6. Key Implementation Details + +### 6.1 Seed Resolution + +| Seed Type | Where Stored | Resolved At | +| --------- | ---------------------- | ---------------------------------------- | +| `ctx.*` | Variant idx field (u8) | On-chain via `post_system_accounts[idx]` | +| `data.*` | Unpacked account data | On-chain via `self.field` | +| Literals | Hardcoded in macro | Compile time | +| Constants | Hardcoded in macro | Compile time | + +### 6.2 Index Space + +All indices (`authority_idx`, `mint_authority_idx`, etc.) reference `remaining_accounts` after system accounts: + +``` +remaining_accounts layout: +[0..system_end]: System accounts (light_system_program, etc.) +[system_end..tail_start]: Packed pubkeys (deduped) +[tail_start..]: Decompressed PDA addresses +``` + +### 6.3 Pack/Unpack Flow + +```rust +// Client: Pack (Pubkey -> u8) +CompressedAccountVariant::UserRecord { data, authority, mint_authority } +-> Pack::pack(&mut remaining_accounts) +-> CompressedAccountVariant::PackedUserRecord { + data: data.pack(&mut remaining_accounts), + authority_idx: remaining_accounts.insert_or_get(authority), + mint_authority_idx: remaining_accounts.insert_or_get(mint_authority), + } + +// On-chain: Unpack (u8 -> Pubkey) +PackedUserRecord { data, authority_idx, mint_authority_idx } +-> Unpack::unpack(post_system_accounts) +-> UserRecord { + data: data.unpack(post_system_accounts)?, + authority: *post_system_accounts[authority_idx].key, + mint_authority: *post_system_accounts[mint_authority_idx].key, + } +``` + +--- + +## 7. Files Reference + +| File | Purpose | +| -------------------------------------------------------- | ------------------------------------------------- | +| `sdk-libs/macros/src/compressible/instructions.rs` | Main macro, generates seeds structs, constructors | +| `sdk-libs/macros/src/compressible/variant_enum.rs` | CompressedAccountVariant enum, Pack/Unpack | +| `sdk-libs/macros/src/compressible/decompress_context.rs` | DecompressContext trait impl | +| `sdk-libs/macros/src/compressible/seed_providers.rs` | CToken seed provider (unchanged) | +| `sdk-libs/compressible-client/src/lib.rs` | Client API types and instruction builders | +| `sdk-tests/csdk-anchor-full-derived-test/` | Complete working example | + +--- + +## 8. Error Codes + +```rust +pub enum CompressibleInstructionError { + InvalidRentSponsor, + MissingSeedAccount, + SeedMismatch, // data.* seeds don't match compressed account data + CTokenDecompressionNotImplemented, + PdaDecompressionNotImplemented, + TokenCompressionNotImplemented, + PdaCompressionNotImplemented, +} +``` + +--- + +## 9. Test Command + +```bash +cargo test-sbf -p csdk-anchor-full-derived-test +``` + +--- + +## 10. Architecture Diagram + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ CLIENT SIDE │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────┐ ┌──────────────────────────────────────┐ │ +│ │ RPC: get_compressed_ │ │ UserRecordSeeds │ │ +│ │ account() │ │ ┌────────────────────────────────┐ │ │ +│ │ ───────────────────► │ │ │ authority: Pubkey (ctx.*) │ │ │ +│ │ CompressedAccount { │ │ │ mint_authority: Pubkey(ctx.*) │ │ │ +│ │ data: bytes, │ │ │ owner: Pubkey (data.*) │ │ │ +│ │ hash, │ │ │ category_id: u64 (data.*) │ │ │ +│ │ tree_info, │ │ └────────────────────────────────┘ │ │ +│ │ } │ └──────────────────────────────────────┘ │ +│ └─────────────────────────┘ │ │ +│ │ │ │ +│ ▼ │ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ AccountInterface::cold(pda_address, compressed_account) │ │ +│ │ - pubkey: Pubkey (target PDA address) │ │ +│ │ - is_cold: true │ │ +│ │ - decompression_context: Some(compressed_account) │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ │ │ +│ └───────────────┬───────────────────┘ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ CompressedAccountVariant::user_record(interface.compressed_data(), seeds)│ +│ │ 1. Deserialize: UserRecord::deserialize(&data_bytes) │ │ +│ │ 2. Verify: data.owner == seeds.owner │ │ +│ │ 3. Verify: data.category_id == seeds.category_id │ │ +│ │ 4. Return: UserRecord { data, authority, mint_authority } │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ RentFreeDecompressAccount::new(account_interface, variant) │ │ +│ │ - account_interface: AccountInterface (pubkey + compressed data) │ │ +│ │ - variant: CompressedAccountVariant::UserRecord { ... } │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ decompress_accounts_idempotent_new( │ │ +│ │ program_id, │ │ +│ │ vec![decompress_account1, decompress_account2, ...], │ │ +│ │ &account_metas, │ │ +│ │ validity_proof, │ │ +│ │ None, // default discriminator │ │ +│ │ ) │ │ +│ │ │ │ +│ │ 1. Filter: keep only is_cold accounts │ │ +│ │ 2. Extract: pubkeys from account_interface │ │ +│ │ 3. Pack::pack() converts Pubkeys to indices │ │ +│ │ 4. Return: Some(Instruction) or None if all hot │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +└──────────────────────────────┼──────────────────────────────────────────────┘ + │ + ══════════════════════╪══════════════════════ TRANSACTION + │ + ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ ON-CHAIN │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ 1. UNPACK (PackedUserRecord → UserRecord) │ │ +│ │ authority = post_system_accounts[authority_idx].key │ │ +│ │ mint_authority = post_system_accounts[mint_authority_idx].key │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ 2. DERIVE PDA │ │ +│ │ ctx_seeds = UserRecordCtxSeeds { authority, mint_authority } │ │ +│ │ seeds = ["user_record", │ │ +│ │ ctx_seeds.authority, │ │ +│ │ ctx_seeds.mint_authority, │ │ +│ │ self.owner, // from unpacked data │ │ +│ │ self.category_id] // from unpacked data │ │ +│ │ derived_pda = find_program_address(seeds, program_id) │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ 3. VERIFY & CREATE │ │ +│ │ assert!(derived_pda == target_account.key) │ │ +│ │ if !exists { create_pda() } │ │ +│ │ write_data(unpacked_data) │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## PHASE 8: CToken Seed Refactor (COMPLETED) + +### Current CToken Flow (Problem) + +``` +Client: + CTokenAccountVariant::Vault // No fields - just an enum tag + + TokenData { owner, mint, amount } + → Pack: variant just CLONED (no packing) + +On-chain: + CTokenSeedProvider::get_seeds(ctx.accounts, remaining_accounts) + → ctx.accounts.cmint.as_ref()?.key() // READS FROM NAMED ACCOUNT! + → derive PDA with ["vault", cmint] +``` + +**Problem**: CToken seed resolution still requires named accounts in `DecompressAccountsIdempotent`. + +### Target CToken Flow + +``` +Client: + CTokenAccountVariant::Vault { cmint: Pubkey } // HAS SEED FIELD! + + TokenData { owner, mint, amount } + → Pack: variant.cmint → cmint_idx (pushed to remaining_accounts) + +On-chain: + Unpack: cmint_idx → post_system_accounts[cmint_idx].key → cmint Pubkey + CTokenSeedProvider::get_seeds(program_id) + → self.cmint // READS FROM VARIANT DIRECTLY! + → derive PDA with ["vault", cmint] +``` + +**Result**: No named seed accounts needed. Same pattern as PDAs. + +### Generated Types (After Refactor) + +```rust +// Unpacked (client-side, with Pubkeys) +pub enum CTokenAccountVariant { + Vault { cmint: Pubkey }, + UserAta { owner: Pubkey, cmint: Pubkey }, // If defined +} + +// Packed (wire format, with indices) +pub enum PackedCTokenAccountVariant { + Vault { cmint_idx: u8 }, + UserAta { owner_idx: u8, cmint_idx: u8 }, +} + +// Pack impl +impl Pack for CTokenAccountVariant { + type Packed = PackedCTokenAccountVariant; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + match self { + CTokenAccountVariant::Vault { cmint } => { + PackedCTokenAccountVariant::Vault { + cmint_idx: remaining_accounts.insert_or_get(*cmint), + } + } + } + } +} + +// Unpack impl +impl Unpack for PackedCTokenAccountVariant { + type Unpacked = CTokenAccountVariant; + + fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result { + match self { + PackedCTokenAccountVariant::Vault { cmint_idx } => { + Ok(CTokenAccountVariant::Vault { + cmint: *remaining_accounts[*cmint_idx as usize].key, + }) + } + } + } +} +``` + +### CTokenSeedProvider Trait Change + +```rust +// BEFORE (requires accounts struct) +pub trait CTokenSeedProvider: Copy { + type Accounts<'info>; + + fn get_seeds<'a, 'info>( + &self, + accounts: &'a Self::Accounts<'info>, // Used for ctx.accounts.cmint + remaining_accounts: &'a [AccountInfo<'info>], + ) -> Result<(Vec>, Pubkey), ProgramError>; +} + +// AFTER (self-contained) +pub trait CTokenSeedProvider: Copy { + fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; + fn get_authority_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; +} +``` + +### Generated CTokenSeedProvider impl + +```rust +impl CTokenSeedProvider for CTokenAccountVariant { + fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError> { + match self { + CTokenAccountVariant::Vault { cmint } => { + // cmint is already resolved Pubkey from variant! + let seeds: &[&[u8]] = &[b"vault", cmint.as_ref()]; + let (pda, bump) = Pubkey::find_program_address(seeds, program_id); + let mut seeds_vec = seeds.iter().map(|s| s.to_vec()).collect::>(); + seeds_vec.push(vec![bump]); + Ok((seeds_vec, pda)) + } + } + } +} +``` + +### DecompressAccountsIdempotent Simplification + +```rust +// BEFORE +pub struct DecompressAccountsIdempotent<'info> { + pub fee_payer: Signer<'info>, + pub config: AccountInfo<'info>, + pub rent_sponsor: UncheckedAccount<'info>, + // CToken static accounts + pub ctoken_rent_sponsor: Option>, + pub ctoken_program: Option>, + pub ctoken_cpi_authority: Option>, + pub ctoken_config: Option>, + // SEED ACCOUNTS (needed by CTokenSeedProvider) + pub authority: Option>, + pub mint_authority: Option>, + pub user: Option>, + pub cmint: Option>, + pub some_account: Option>, +} + +// AFTER +pub struct DecompressAccountsIdempotent<'info> { + pub fee_payer: Signer<'info>, + pub config: AccountInfo<'info>, + pub rent_sponsor: UncheckedAccount<'info>, + // CToken static accounts + pub ctoken_rent_sponsor: Option>, + pub ctoken_program: Option>, + pub ctoken_cpi_authority: Option>, + pub ctoken_config: Option>, + // NO SEED ACCOUNTS - they're in the variant! +} +``` + +### Client Usage (After Refactor) + +```rust +// Construct CToken variant with seed pubkeys +let vault_variant = CTokenAccountVariant::Vault { cmint: cmint_pda }; +let ctoken_data = CTokenData { + variant: vault_variant, + token_data: compressed_vault.token.clone(), +}; + +let decompress_instruction = compressible_instruction::decompress_accounts_idempotent_new( + &program_id, + vec![ + RentFreeDecompressAccount::new(user_interface, user_variant), + RentFreeDecompressAccount::new(vault_interface, CompressedAccountVariant::CTokenData { data: ctoken_data }), + ], + compressible_instruction::decompress::accounts(payer.pubkey(), config_pda, payer.pubkey()), + rpc_result, +)?; +``` + +### decompress::accounts Helper + +```rust +/// Returns program account metas for decompress_accounts_idempotent with CToken support. +/// Includes ctoken_rent_sponsor, ctoken_program, ctoken_cpi_authority, ctoken_config. +pub fn accounts(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey) -> Vec; + +/// Returns program account metas for PDA-only decompression (no CToken accounts). +pub fn accounts_pda_only(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey) -> Vec; +``` + +### SDK Changes Required + +| File | Changes | +| --------------------------------------------------- | -------------------------------------------------- | +| `ctoken-sdk/src/pack.rs` | Add Pack bound to V, use V::Packed for packed type | +| `sdk/src/compressible/decompress_runtime.rs` | Update CTokenSeedProvider trait signature | +| `macros/src/compressible/variant_enum.rs` | Generate CTokenAccountVariant with struct fields | +| `macros/src/compressible/seed_providers.rs` | Update get_seeds to use self.field | +| `macros/src/compressible/instructions.rs` | Remove seed account fields from Accounts struct | +| `ctoken-sdk/src/compressible/decompress_runtime.rs` | Update process_decompress_tokens_runtime | + +### Flow Diagram (After Phase 8) + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ CLIENT SIDE │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ CTokenAccountVariant::Vault { cmint: cmint_pda } │ +│ + TokenData { owner, mint, amount } │ +│ = CTokenData { variant, token_data } │ +│ │ │ +│ ▼ │ +│ Pack::pack() │ +│ variant.cmint → cmint_idx = remaining_accounts.insert_or_get(cmint) │ +│ token_data.owner → owner_idx │ +│ token_data.mint → mint_idx │ +│ = PackedCTokenData { variant: Vault { cmint_idx }, token_data } │ +│ │ │ +└──────────────────────────────┼──────────────────────────────────────────────┘ + │ + ══════════════════════╪══════════════════════ TRANSACTION + │ + ▼ +┌────────────────────────────────────────────────────────────────────────────┐ +│ ON-CHAIN │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ PackedCTokenData { variant: Vault { cmint_idx }, token_data } │ +│ │ │ +│ ▼ │ +│ Unpack::unpack(post_system_accounts) │ +│ cmint = post_system_accounts[cmint_idx].key │ +│ owner = post_system_accounts[owner_idx].key │ +│ = CTokenData { variant: Vault { cmint }, token_data } │ +│ │ │ +│ ▼ │ +│ CTokenSeedProvider::get_seeds(program_id) │ +│ match self { │ +│ Vault { cmint } => seeds = ["vault", cmint.as_ref()] │ +│ } │ +│ = (seeds, derived_pda) │ +│ │ │ +│ ▼ │ +│ Verify: derived_pda == target_account.key │ +│ Create token account with seeds │ +│ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +### Implementation Steps + +1. **Update SDK trait** (`sdk/src/compressible/decompress_runtime.rs`): + - Change `CTokenSeedProvider` signature to not require `accounts` param + +2. **Update ctoken-sdk Pack** (`ctoken-sdk/src/pack.rs`): + - Add `Pack` trait bound to `V` in `CTokenDataWithVariant` + - Use `V::Packed` as the packed variant type + +3. **Generate CToken variant enums** (`variant_enum.rs`): + - Parse token_seeds to extract ctx.\* fields + - Generate `CTokenAccountVariant` with struct variants (Pubkeys) + - Generate `PackedCTokenAccountVariant` with struct variants (indices) + - Generate Pack/Unpack impls + +4. **Update seed provider generation** (`seed_providers.rs`): + - Change `get_seeds()` to use `self.cmint` instead of `ctx.accounts.cmint` + +5. **Remove seed accounts** (`instructions.rs`): + - Remove seed account fields from `DecompressAccountsIdempotent` + +6. **Update tests** (`basic_test.rs`): + - Construct `CTokenAccountVariant::Vault { cmint }` with Pubkey + - Remove seed accounts from instruction building diff --git a/sdk-libs/macros/MACRO_REFACTOR.md b/sdk-libs/macros/MACRO_REFACTOR.md new file mode 100644 index 0000000000..efc17efddb --- /dev/null +++ b/sdk-libs/macros/MACRO_REFACTOR.md @@ -0,0 +1,518 @@ +# Compressible Macro Refactor Plan + +## Goal + +Eliminate seed duplication by extracting seeds from Anchor's `#[account(seeds = [...])]` attributes instead of requiring separate declaration in `#[compressible(...)]`. + +--- + +## Current Architecture (Problems) + +### Dual Seed Declaration + +```rust +// Declaration 1: Anchor attribute (source of truth for on-chain PDA) +#[account( + seeds = [b"user_record", authority.key().as_ref(), params.owner.as_ref()], + bump, +)] +pub user_record: Account<'info, UserRecord>, + +// Declaration 2: Global compressible macro (DUPLICATED) +#[compressible( + UserRecord = (seeds = ("user_record", ctx.authority, data.owner)), + owner = Pubkey, +)] +``` + +**Problems:** + +- Seeds declared twice - can diverge +- Refactoring risk - change one, forget the other +- Runtime failures from seed mismatch +- Cognitive overhead + +### Current Generated Items + +From global `#[compressible(...)]`: + +- `CompressedAccountVariant` enum +- `PackedXxx` structs per type +- `SeedParams` struct for instruction data fields +- `DecompressAccountsIdempotent<'info>` with **named** seed accounts +- `CompressAccountsIdempotent<'info>` +- `PdaSeedDerivation` trait impls +- `CTokenSeedProvider` trait impls +- Instruction handlers +- Client-side seed functions + +--- + +## Proposed Architecture + +### Single Source of Truth + +Seeds extracted from Anchor's `#[account(seeds = [...])]` attribute: + +```rust +#[derive(Accounts, LightCompressible)] +#[instruction(params: MyParams)] +pub struct CreateUserRecord<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + UserRecord::INIT_SPACE, + seeds = [b"user_record", authority.key().as_ref(), params.owner.as_ref()], + bump, + )] + #[compressible( + address_tree_info = params.address_tree_info, + output_tree = params.output_state_tree_index + )] + pub user_record: Account<'info, UserRecord>, + + pub system_program: Program<'info, System>, +} +``` + +**No duplicate seed declaration needed.** + +### Token Accounts + +For CToken accounts, reference the authority field directly: + +```rust +#[account(mut, seeds = [b"vault", cmint.key().as_ref()], bump)] +#[compressible_token( + address_tree_info = params.address_tree_info, + output_tree = params.output_state_tree_index, + authority = vault_authority, // Reference to authority field +)] +pub vault: UncheckedAccount<'info>, + +#[account(seeds = [b"vault_authority"], bump)] +pub vault_authority: UncheckedAccount<'info>, // Authority seeds extracted from here +``` + +### Token Authority Resolution + +The authority for a CToken account can be various types. The macro auto-detects or allows explicit override. + +#### Authority Types + +| Authority Type | Example | Seeds Needed? | +| ---------------- | ------------------------------------------------ | ----------------------------------- | +| PDA | `#[account(seeds = [b"vault_authority"], bump)]` | Yes - extract from field | +| Signer | `pub authority: Signer<'info>` | No - user signs directly | +| External/Dynamic | Stored in account data, passed differently | Explicit `authority_seeds` required | + +#### Auto-Detection Logic + +```rust +fn resolve_authority_seeds(token_field: &Field, accounts_struct: &ItemStruct) -> AuthoritySeeds { + let authority_field_name = get_authority_from_attr(token_field); + let authority_field = find_field(accounts_struct, authority_field_name); + + // Case 1: Field is Signer<'info> - user signs, no seeds needed + if is_signer_type(&authority_field.ty) { + return AuthoritySeeds::UserSigns; + } + + // Case 2: Field has #[account(seeds = [...])] - extract them + if let Some(seeds) = extract_anchor_seeds(&authority_field) { + return AuthoritySeeds::Pda(seeds); + } + + // Case 3: Check for explicit authority_seeds in attribute + if let Some(seeds) = get_explicit_authority_seeds(token_field) { + return AuthoritySeeds::Pda(seeds); + } + + // Case 4: Can't determine - compile error + compile_error!( + "Cannot determine authority seeds. Either:\n\ + - Add #[account(seeds = [...])] to the authority field, or\n\ + - Add authority_seeds = (...) to #[compressible_token], or\n\ + - Use Signer<'info> if user signs directly" + ) +} +``` + +#### Examples + +**PDA authority (auto-detected):** + +```rust +#[compressible_token(authority = vault_authority)] +pub vault: UncheckedAccount<'info>, + +#[account(seeds = [b"vault_authority"], bump)] // macro extracts these +pub vault_authority: UncheckedAccount<'info>, +``` + +**User signer (auto-detected):** + +```rust +#[compressible_token(authority = user)] +pub vault: UncheckedAccount<'info>, + +pub user: Signer<'info>, // macro sees Signer, no seeds needed +``` + +**Complex/dynamic seeds (explicit override):** + +```rust +#[compressible_token( + authority = pool_authority, + authority_seeds = (POOL_AUTH_SEED, pool_state.key()), // explicit +)] +pub vault: UncheckedAccount<'info>, + +/// CHECK: Authority derived from pool state +pub pool_authority: UncheckedAccount<'info>, // no #[account(seeds)] here +``` + +**External authority (must sign tx):** + +```rust +#[compressible_token( + authority = external_authority, + authority_is_signer, // authority must sign the tx directly +)] +pub vault: UncheckedAccount<'info>, + +/// CHECK: External multisig or other authority +pub external_authority: UncheckedAccount<'info>, +``` + +#### Attribute Spec + +```rust +#[compressible_token( + address_tree_info = , + output_tree = , + authority = , // Required: which field is the authority + + // Optional (mutually exclusive) - only needed if auto-detect fails: + authority_seeds = (, ...), // Explicit PDA seeds + authority_is_signer, // Authority signs tx directly (no PDA) +)] +``` + +### Complex Seed Expressions + +Authority seeds can contain arbitrary expressions: + +```rust +#[account( + seeds = [ + b"vault_authority", // Byte literal + VAULT_AUTH_SEED, // Constant + pool.key().as_ref(), // Account reference + params.pool_id.as_ref(), // Param reference + params.nonce.to_le_bytes().as_ref(), // Param with conversion + max_key(&a.key(), &b.key()).as_ref(), // Function call + ], + bump, +)] +pub vault_authority: UncheckedAccount<'info>, +``` + +#### Handling Strategy + +| Expression Type | Auto-detect? | Notes | +| ---------------------------- | ------------ | ------------------------------------- | +| `b"literal"` | Yes | Hardcoded | +| `CONSTANT` | Yes | Resolved at compile time | +| `account.key()` | Yes | Account must be in struct | +| `params.field` | Yes | If `#[instruction]` parsed | +| `params.field.to_le_bytes()` | Yes | Same, with method chain | +| `&account.data.field[..]` | Pass-through | Account must be deserialized | +| `function(args)` | Pass-through | Emitted as-is, compile error if wrong | + +#### Pass-Through Approach + +For expressions we can't fully classify, emit them as-is with rewrite rules: + +```rust +// Input: seeds = [VAULT_AUTH_SEED, max_key(&a.key(), &b.key()).as_ref()] + +// Generated code (rewritten): +fn derive_authority_seeds<'info>( + accounts: &MyAccounts<'info>, + _params: &MyParams, +) -> Result>, ProgramError> { + let seeds: Vec<&[u8]> = vec![ + VAULT_AUTH_SEED, // constant - direct + max_key(&accounts.a.key(), &accounts.b.key()).as_ref(), // rewritten: a -> accounts.a + ]; + // ... +} +``` + +**Rewrite rules:** + +- `field.key()` → `accounts.field.key()` +- `params.x` → `params.x` +- Everything else → pass through unchanged + +#### Failure Modes + +| Scenario | Result | Fix | +| --------------------- | ------------- | -------------------------------------- | +| Function not in scope | Compile error | Import the function | +| Account not in struct | Compile error | Add account to struct | +| Wrong type | Compile error | Fix types | +| Runtime logic differs | Runtime error | Use explicit `authority_seeds = (...)` | + +All failures are **explicit errors**, not silent bugs. + +### Module-Level Declaration (Minimal) + +Only need to declare which types form the enum: + +```rust +#[compressible_types(UserRecord, GameSession, PlaceholderRecord)] +#[program] +pub mod my_program { + // ... +} +``` + +Or potentially infer from `#[compressible]` fields across all Accounts structs. + +--- + +## Generated Items (Refactored) + +### Per-Account-Type Seed Struct + +From parsing `#[account(seeds = [b"user_record", authority.key().as_ref(), params.owner.as_ref()])]`: + +```rust +// Generated +pub struct UserRecordSeeds { + pub authority: Pubkey, // from `authority.key().as_ref()` + pub owner: Pubkey, // from `params.owner.as_ref()` +} + +impl UserRecordSeeds { + pub fn derive_pda(&self, program_id: &Pubkey) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[b"user_record", self.authority.as_ref(), self.owner.as_ref()], + program_id, + ) + } +} +``` + +### Seed Classification + +The macro classifies each seed expression: + +| Expression Type | Classification | Generated Field | +| -------------------------- | ----------------- | -------------------------------------------- | +| `b"literal"` | Literal | (none - hardcoded) | +| `authority.key().as_ref()` | Account reference | `authority: Pubkey` | +| `params.owner.as_ref()` | Instruction data | `owner: Pubkey` (type from `#[instruction]`) | +| `CONSTANT` | Constant | (none - resolved at compile time) | + +### DecompressAccountsIdempotent (Refactored) + +**Option A: Remaining Accounts with Typed Indices** + +```rust +#[derive(Accounts)] +pub struct DecompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// CHECK: Validated by SDK + pub config: AccountInfo<'info>, + /// CHECK: Validated by SDK + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + // ... standard accounts only, NO named seed accounts +} + +// Client builds remaining_accounts with seed accounts +// SDK provides index mapping per variant +``` + +**Option B: Keep Named Optional Accounts (simpler)** + +Keep current approach but auto-generate from parsed seeds: + +```rust +#[derive(Accounts)] +pub struct DecompressAccountsIdempotent<'info> { + // ... standard accounts ... + + // Auto-generated from all unique account refs in seeds + /// CHECK: Optional seed account + #[account(mut)] + pub authority: Option>, + /// CHECK: Optional seed account + pub mint_authority: Option>, +} +``` + +--- + +## Trait Changes + +### PdaSeedDerivation + +```rust +// Current: Accounts struct + SeedParams +impl PdaSeedDerivation, SeedParams> for UserRecord { + fn derive_pda_seeds_with_accounts(&self, ..., accounts: &DecompressAccountsIdempotent, seed_params: &SeedParams) -> ... +} + +// Proposed: Just the typed seeds struct +impl PdaSeedDerivation for UserRecord { + type Seeds = UserRecordSeeds; + + fn derive_pda(seeds: &Self::Seeds, program_id: &Pubkey) -> (Pubkey, u8) { + seeds.derive_pda(program_id) + } +} +``` + +--- + +## Migration Path + +### Phase 1: Add New Derive Macro + +- Implement `LightCompressible` derive macro +- Parses `#[account(seeds = [...])]` from Anchor attribute +- Generates `XxxSeeds` structs +- Coexists with current `#[compressible(...)]` + +### Phase 2: Update DecompressAccountsIdempotent Generation + +- Auto-generate from parsed seeds across all Accounts structs +- Or use remaining_accounts approach +- Update `process_decompress_accounts_idempotent` to use new traits + +### Phase 3: Simplify Module-Level Macro + +- Reduce to just type list: `#[compressible_types(UserRecord, GameSession)]` +- Or remove entirely if types can be inferred + +### Phase 4: Deprecate Old Syntax + +- Emit warnings for old `#[compressible(Type = (seeds = ...))]` syntax +- Eventually remove + +--- + +## Implementation Details + +### Parsing Anchor Seeds Attribute + +```rust +fn extract_anchor_seeds(field: &syn::Field) -> Option> { + for attr in &field.attrs { + if attr.path().is_ident("account") { + // Parse: #[account(seeds = [...], bump, ...)] + // Extract the seeds = [...] part + // Return parsed seed expressions + } + } + None +} + +enum SeedExpr { + Literal(Vec), // b"user_record" + AccountRef(Ident), // authority.key().as_ref() -> authority + ParamRef(Ident, Type), // params.owner.as_ref() -> (owner, Pubkey) + Constant(Path), // MY_SEED +} +``` + +### Extracting Type from #[instruction] + +```rust +#[derive(Accounts, LightCompressible)] +#[instruction(params: MyParams)] +pub struct CreateUserRecord<'info> { ... } + +// Macro reads #[instruction(params: MyParams)] +// Then resolves MyParams to get field types for params.xxx references +``` + +This requires either: + +1. The params type to be in the same module (can resolve) +2. User annotation: `#[compressible(params_type = MyParams)]` +3. Accept just the field name, infer type as Pubkey/u64/etc. + +--- + +## Client-Side Changes + +### Current + +```rust +let decompress_account = CompressedAccountData { + data: CompressedAccountVariant::UserRecord(packed_data), + meta: compressed_account_meta, +}; + +// + SeedParams struct +// + named accounts in instruction +``` + +### Proposed + +```rust +let decompress_input = DecompressInput { + variant: CompressedAccountVariant::UserRecord(packed_data), + seeds: UserRecordSeeds { authority, owner }, // Typed! + compressed_account: compressed_account_data, +}; + +// Seeds struct is type-safe, IDE autocomplete works +``` + +--- + +## Open Questions + +1. **Remaining accounts vs named optional accounts for decompress?** + - Named: simpler, current approach, more readable + - Remaining: more flexible, less struct bloat + +2. ~~**How to handle token authority seeds?**~~ **RESOLVED** + - Auto-detect from authority field's `#[account(seeds)]` or `Signer` type + - Explicit `authority_seeds = (...)` as fallback + - `authority_is_signer` for external signers + - See "Token Authority Resolution" section + +3. **Type inference for params.xxx references?** + - Parse `#[instruction]` attribute for type + - Or require explicit annotation + - Or default to common types (Pubkey, u64) + +4. **Enum generation without module-level macro?** + - Scan all files for `#[compressible]` fields? + - Explicit type list still needed? + +--- + +## Benefits Summary + +| Aspect | Current | Proposed | +| ---------------------- | ------------------------- | -------------------- | +| Seed declarations | 2 (Anchor + compressible) | 1 (Anchor only) | +| Sync bugs possible | Yes | No | +| Refactoring safety | Low | High | +| Type-safe seed structs | Partial | Full | +| IDE support | Limited | Better (typed seeds) | +| Maintenance burden | High | Low | diff --git a/sdk-libs/macros/MACRO_REFACTOR_V2.md b/sdk-libs/macros/MACRO_REFACTOR_V2.md new file mode 100644 index 0000000000..091bb27610 --- /dev/null +++ b/sdk-libs/macros/MACRO_REFACTOR_V2.md @@ -0,0 +1,642 @@ +# Compressible Macro Refactor V2 - Single Source of Truth + +## Status: ✅ FULLY IMPLEMENTED + +### What Works: + +- `#[compressible_program]` macro - **works across separate files!** +- PDA seed extraction from Anchor `#[account(seeds = [...])]` works +- Token field `#[compressible_token(Variant, authority = [...])]` extraction and codegen works +- Supports: byte literals, string literals, constants, `ctx.field.key()`, `params.field`, function calls +- File scanner recursively reads all `.rs` files in `src/` directory at compile time + +### How It Works: + +The `#[compressible_program]` macro bypasses proc macro limitations by directly reading and parsing +source files from the crate's `src/` directory. This "hidden export/import" pattern allows it to: + +1. Find all `#[derive(Accounts)]` structs across any file +2. Extract `#[compressible]` and `#[compressible_token]` marked fields +3. Parse their `#[account(seeds = [...])]` attributes +4. Generate all required code in the program module + +### Usage: + +```rust +// lib.rs - just add the macro, seeds come from Accounts structs automatically! +#[compressible_program] +#[program] +pub mod my_program { ... } + +// instruction_accounts.rs (separate file - works!) +#[derive(Accounts, LightFinalize)] +pub struct CreateAccounts<'info> { + #[account(seeds = [b"user", authority.key().as_ref()], bump)] + #[compressible] + pub user_record: Account<'info, UserRecord>, + + #[account(seeds = [b"vault", cmint.key().as_ref()], bump)] + #[compressible_token(Vault, authority = [b"vault_authority"])] + pub vault: UncheckedAccount<'info>, +} +``` + +--- + +## Executive Summary + +Replace the dual-declaration system with a single source of truth: + +- **BEFORE**: Seeds declared twice (global `#[compressible(...)]` + Anchor `#[account(seeds)]`) +- **AFTER**: Seeds extracted from Anchor's `#[account(seeds)]` attribute automatically + +--- + +## System Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ COMPILE TIME │ +├─────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────┐ ┌─────────────────────────────────────────────┐ │ +│ │ Module Level Macro │ │ Accounts Struct (can be in separate file!) │ │ +│ │ │ │ │ │ +│ │ #[compressible_program]│ │ #[derive(Accounts, LightFinalize)] │ │ +│ │ #[program] │ │ #[instruction(params: MyParams)] │ │ +│ │ pub mod my_program {} │ │ pub struct CreateAccounts<'info> { │ │ +│ │ │ │ │ │ +│ │ (File scanner reads │ │ #[account( │ │ +│ │ all .rs files in │ │ init, payer = fee_payer, │ │ +│ │ src/ directory) │ │ seeds = [b"user", auth.key().as_ref(), │ │ +│ │ │ │ params.owner.as_ref()], │ │ +│ └────────────┬────────────┘ │ bump, │ │ +│ │ │ )] │ │ +│ │ Scans for │ #[compressible] │ │ +│ │ #[compressible] │ pub user: Account<'info, UserRecord>, │ │ +│ │ fields │ │ │ +│ │ │ #[account(seeds = [b"vault", cmint...], │ │ +│ │ │ bump)] │ │ +│ │ │ #[compressible_token(Vault, authority=.)]│ │ +│ │ │ pub vault: UncheckedAccount<'info>, │ │ +│ │ │ } │ │ +│ │ └───────────────────┬─────────────────────────┘ │ +│ │ │ │ +│ │ │ Provides seeds, types │ +│ │ │ │ +│ └──────────────┬───────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ CODEGEN OUTPUT │ │ +│ │ │ │ +│ │ 1. CompressedAccountVariant enum (with struct variants) │ │ +│ │ 2. PackedCompressedAccountVariant (with idx fields) │ │ +│ │ 3. CTokenAccountVariant enum │ │ +│ │ 4. Pack/Unpack impls │ │ +│ │ 5. XxxSeeds structs per PDA type │ │ +│ │ 6. PdaSeedDerivation trait impls │ │ +│ │ 7. CTokenSeedProvider trait impls │ │ +│ │ 8. DecompressAccountsIdempotent Accounts struct │ │ +│ │ 9. decompress_accounts_idempotent() instruction handler │ │ +│ │ 10. Client-side seed derivation functions │ │ +│ │ │ │ +│ └───────────────────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Seed Extraction Flow + +``` +┌──────────────────────────────────────────────────────────────────────────────────────────┐ +│ ANCHOR ATTRIBUTE PARSING │ +├──────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ Input: #[account(seeds = [b"user", auth.key().as_ref(), params.owner.as_ref()], bump)] │ +│ │ │ +│ ▼ │ +│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ +│ │ SEED EXPRESSION PARSER │ │ +│ │ │ │ +│ │ For each element in seeds array: │ │ +│ │ │ │ +│ │ ┌─────────────────┬────────────────────────────────────────────────────────┐ │ │ +│ │ │ Expression Type │ Classification & Generated Code │ │ │ +│ │ ├─────────────────┼────────────────────────────────────────────────────────┤ │ │ +│ │ │ b"literal" │ Literal → Hardcoded: &[0x75, 0x73, 0x65, 0x72] │ │ │ +│ │ │ "string" │ Literal → Hardcoded: "string".as_bytes() │ │ │ +│ │ │ CONSTANT │ Constant → crate::CONSTANT.as_ref() │ │ │ +│ │ │ auth.key() │ CtxAccount → ctx_seeds.auth field (Pubkey) │ │ │ +│ │ │ params.owner │ DataField → self.owner from deserialized data │ │ │ +│ │ │ params.id.to_le_bytes() │ DataField → self.id.to_le_bytes() │ │ │ +│ │ │ max_key(&a,&b) │ FnCall → pass through with field mapping │ │ │ +│ │ └─────────────────┴────────────────────────────────────────────────────────┘ │ │ +│ └──────────────────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ Output: SeedSpec { literals, ctx_fields: [auth], data_fields: [owner] } │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Account Type Detection (Robustness) + +``` +┌───────────────────────────────────────────────────────────────────────────────────────────┐ +│ SUPPORTED ANCHOR ACCOUNT TYPES │ +├───────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌────────────────────────────────────────┬───────────────────────────────────────────┐ │ +│ │ Type Pattern │ Extraction Strategy │ │ +│ ├────────────────────────────────────────┼───────────────────────────────────────────┤ │ +│ │ Account<'info, T> │ Direct: inner_type = T │ │ +│ │ Box> │ Unwrap Box: inner_type = T │ │ +│ │ AccountLoader<'info, T> │ Direct: inner_type = T (zero-copy) │ │ +│ │ InterfaceAccount<'info, T> │ Direct: inner_type = T (SPL interface) │ │ +│ │ Box> │ Unwrap Box: inner_type = T │ │ +│ │ UncheckedAccount<'info> │ No type - for tokens only (explicit map) │ │ +│ │ AccountInfo<'info> │ No type - for tokens only (explicit map) │ │ +│ └────────────────────────────────────────┴───────────────────────────────────────────┘ │ +│ │ +│ DETECTION ALGORITHM: │ +│ │ +│ fn extract_account_inner_type(ty: &Type) -> Option<(bool, Ident)> { │ +│ match ty { │ +│ // Direct Account │ +│ Type::Path { segments: [.., Segment { ident: "Account", args: <'_, T> }] } │ +│ => Some((false, T)) │ +│ │ +│ // Box> │ +│ Type::Path { segments: [.., Segment { ident: "Box", args: > }] } │ +│ => Some((true, T)) │ +│ │ +│ // AccountLoader │ +│ Type::Path { segments: [.., Segment { ident: "AccountLoader", args: <'_, T> }] } │ +│ => Some((false, T)) │ +│ │ +│ // InterfaceAccount │ +│ Type::Path { segments: [.., Segment { ident: "InterfaceAccount", args }] } │ +│ => Some((false, T)) │ +│ │ +│ // Box> │ +│ Type::Path { segments: [.., "Box", args: > }] } │ +│ => Some((true, T)) │ +│ │ +│ _ => None // UncheckedAccount, AccountInfo - no inner type │ +│ } │ +│ } │ +│ │ +└───────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Data Flow: Creation to Decompression + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ CREATION FLOW │ +├─────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ CLIENT │ +│ ─────── │ +│ 1. Derive PDA address: Pubkey::find_program_address(seeds, program_id) │ +│ 2. Call get_create_accounts_proof(pda_addresses) │ +│ 3. Build instruction with params containing create_accounts_proof │ +│ │ +│ │ │ +│ ▼ │ +│ ON-CHAIN (pre_init) │ +│ ──────────────────── │ +│ 1. LightFinalize parses #[compressible] fields │ +│ 2. For each field: │ +│ - Extract seeds from Anchor #[account(seeds = [...])] │ +│ - Derive compressed address: derive_address(pda_key, tree, program_id) │ +│ - Write to CPI context OR invoke Light System Program │ +│ 3. PDA initialized on-chain + compressed address registered │ +│ │ +│ │ │ +│ ▼ │ +│ RESULT │ +│ ────── │ +│ - On-chain PDA at: Pubkey::find_program_address(seeds, program_id) │ +│ - Compressed address at: derive_address(pda_key, tree, program_id) │ +│ - Data written to PDA │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ DECOMPRESSION FLOW │ +├─────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ CLIENT │ +│ ─────── │ +│ 1. Fetch compressed account from indexer (by compressed address) │ +│ 2. Create AccountInterface::cold(pda_address, compressed_account) │ +│ 3. Create variant with seeds: │ +│ CompressedAccountVariant::user_record( │ +│ interface.compressed_data(), │ +│ UserRecordSeeds { auth, owner } // ctx.* + data.* seeds │ +│ ) │ +│ 4. Pack variant → indices into remaining_accounts │ +│ 5. Build & send decompress_accounts_idempotent instruction │ +│ │ +│ │ │ +│ ▼ │ +│ ON-CHAIN (decompress_accounts_idempotent) │ +│ ────────────────────────────────────────── │ +│ 1. Unpack: idx fields → Pubkey from remaining_accounts │ +│ 2. Deserialize compressed account data │ +│ 3. Build seeds: [literal, ctx_seeds.auth, self.owner, ...] │ +│ 4. Derive PDA: Pubkey::find_program_address(seeds, program_id) │ +│ 5. Verify: derived_pda == target_account.key │ +│ 6. Create PDA if not exists, write data │ +│ │ +│ │ │ +│ ▼ │ +│ RESULT │ +│ ────── │ +│ - On-chain PDA recreated with original data │ +│ - Compressed account consumed (nullified) │ +│ │ +└─────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## New Syntax Specification + +### Module Level (Simplified) + +```rust +// BEFORE (old - verbose, error-prone, seeds declared twice) +#[compressible( + UserRecord = (seeds = ("user_record", ctx.authority, data.owner, data.category_id.to_le_bytes())), + GameSession = (seeds = (GAME_SESSION_SEED, max_key(&ctx.user.key(), &ctx.authority.key()), data.session_id.to_le_bytes())), + Vault = (is_token, seeds = ("vault", ctx.cmint), authority = ("vault_authority")), + owner = Pubkey, + category_id = u64, + session_id = u64, +)] +#[program] +pub mod my_program { ... } + +// AFTER (new - no type list needed! Seeds extracted from Accounts structs) +#[compressible_program] // Just this! Scans src/ for #[compressible] fields +#[program] +pub mod my_program { ... } +``` + +### Accounts Struct (Seeds from Anchor) + +```rust +#[derive(Accounts, LightFinalize)] +#[instruction(params: CreateParams)] +pub struct CreateAccounts<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + // PDA - type extracted, seeds from #[account(...)] + #[account( + init, + payer = fee_payer, + space = 8 + UserRecord::INIT_SPACE, + seeds = [b"user_record", authority.key().as_ref(), params.owner.as_ref()], + bump, + )] + #[compressible] // Marker only - no seed duplication + pub user_record: Account<'info, UserRecord>, + + // Also works with Box + #[account( + init, + payer = fee_payer, + space = 8 + GameSession::INIT_SPACE, + seeds = [GAME_SESSION_SEED.as_bytes(), max_key(&fee_payer.key(), &authority.key()).as_ref()], + bump, + )] + #[compressible] + pub game_session: Box>, + + // Token - explicit variant + authority seeds (required for UncheckedAccount) + #[account( + mut, + seeds = [b"vault", cmint.key().as_ref()], + bump, + )] + #[compressible_token(Vault, authority = [b"vault_authority"])] // Variant + authority seeds + pub vault: UncheckedAccount<'info>, + + pub compression_config: AccountInfo<'info>, + pub system_program: Program<'info, System>, +} +``` + +--- + +## Macro Implementation (DONE ✅) + +### Files Modified + +| File | Purpose | Status | +| ------------------------------------------- | ---------------------------- | ------------------------------------------ | +| `macros/src/lib.rs` | Entry points | ✅ Added `compressible_program` proc macro | +| `macros/src/compressible/instructions.rs` | Generate code from seeds | ✅ Refactored for new seed source | +| `macros/src/compressible/file_scanner.rs` | **NEW** Scan src/ for fields | ✅ Implemented - reads external .rs files | +| `macros/src/compressible/anchor_seeds.rs` | Extract seeds from Anchor | ✅ Full seed classification | +| `macros/src/compressible/variant_enum.rs` | Generate enum | ✅ Uses extracted seed info | +| `macros/src/compressible/seed_providers.rs` | CToken seed provider | ✅ Adapted for new format | + +### New Parsing Logic + +```rust +// In finalize/parse.rs + +/// Parsed seed element from Anchor #[account(seeds = [...])] +#[derive(Clone, Debug)] +pub enum ParsedSeedElement { + /// b"literal" or "string" + Literal(Vec), + /// Compile-time constant: SOME_SEED + Constant(syn::Path), + /// Account reference: authority.key().as_ref() + CtxAccount(syn::Ident), + /// Param/data reference: params.owner.as_ref() + DataField { + field_name: syn::Ident, + method_chain: Option, // e.g., to_le_bytes + }, + /// Function call: max_key(&a.key(), &b.key()) + FunctionCall { + func: syn::Path, + ctx_args: Vec, // Account references in args + }, +} + +/// Extract seeds from Anchor #[account(seeds = [...], bump)] attribute +fn extract_anchor_seeds(field: &syn::Field) -> Option> { + for attr in &field.attrs { + if !attr.path().is_ident("account") { + continue; + } + + // Parse the attribute content + let meta_list = attr.parse_args_with( + Punctuated::::parse_terminated + ).ok()?; + + for meta in meta_list { + if let Meta::NameValue(nv) = meta { + if nv.path.is_ident("seeds") { + // Parse seeds = [...] + return parse_seeds_array(&nv.value); + } + } + } + } + None +} + +/// Classify a seed expression +fn classify_seed_expr(expr: &syn::Expr) -> ParsedSeedElement { + match expr { + // b"literal" + Expr::Lit(ExprLit { lit: Lit::ByteStr(bs), .. }) => { + ParsedSeedElement::Literal(bs.value()) + } + + // "string".as_bytes() or just "string" + Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) => { + ParsedSeedElement::Literal(s.value().into_bytes()) + } + + // CONSTANT (all uppercase) + Expr::Path(path) if is_constant_path(&path.path) => { + ParsedSeedElement::Constant(path.path.clone()) + } + + // authority.key().as_ref() -> CtxAccount + Expr::MethodCall(mc) if is_ctx_account_ref(mc) => { + let field_name = extract_receiver_ident(mc); + ParsedSeedElement::CtxAccount(field_name) + } + + // params.owner.as_ref() -> DataField + Expr::MethodCall(mc) if is_params_field_ref(mc) => { + let (field_name, method) = extract_params_field(mc); + ParsedSeedElement::DataField { field_name, method_chain: method } + } + + // max_key(&a.key(), &b.key()).as_ref() -> FunctionCall + Expr::MethodCall(mc) if is_function_call_ref(mc) => { + let (func, ctx_args) = extract_function_call(mc); + ParsedSeedElement::FunctionCall { func, ctx_args } + } + + _ => panic!("Unsupported seed expression: {:?}", expr), + } +} +``` + +--- + +## Generated Code Examples + +### Seeds Struct + +```rust +// Generated from extracted seeds +pub struct UserRecordSeeds { + // From ctx.* seed elements + pub authority: Pubkey, + // From data.* seed elements (for verification) + pub owner: Pubkey, + pub category_id: u64, +} + +impl UserRecordSeeds { + pub fn derive_pda(&self, program_id: &Pubkey) -> (Pubkey, u8) { + let seeds: &[&[u8]] = &[ + b"user_record", + self.authority.as_ref(), + self.owner.as_ref(), + &self.category_id.to_le_bytes(), + ]; + Pubkey::find_program_address(seeds, program_id) + } +} +``` + +### Variant Constructor + +```rust +impl CompressedAccountVariant { + pub fn user_record( + account_data: &[u8], + seeds: UserRecordSeeds, + ) -> Result { + let data = UserRecord::deserialize(&mut &account_data[..])?; + + // Verify data.* seeds match compressed account + if data.owner != seeds.owner { + return Err(CompressibleInstructionError::SeedMismatch.into()); + } + if data.category_id != seeds.category_id { + return Err(CompressibleInstructionError::SeedMismatch.into()); + } + + Ok(Self::UserRecord { + data, + authority: seeds.authority, + }) + } +} +``` + +--- + +## Footguns & Robustness + +### 1. Type Listed but No Matching Account Field + +**Scenario**: `#[compressible_types(UserRecord)]` but no `Account` field + +**Solution**: Compile-time error with clear message + +``` +error: Type 'UserRecord' listed in #[compressible_types] but no matching + Account or Box> field found in any + #[derive(Accounts)] struct with #[compressible] attribute. + --> lib.rs:50:1 +``` + +### 2. Multiple Instructions with Different Seeds + +**Scenario**: Same type used with different seeds in different instructions + +**Solution**: Currently unsupported - emit error + +``` +error: Type 'UserRecord' has conflicting seed definitions: + - In CreateUserRecord: seeds = [b"user_v1", ...] + - In MigrateUserRecord: seeds = [b"user_v2", ...] + +Consider using different types for different PDA schemes. +``` + +### 3. Seed Expression Not Recognized + +**Scenario**: Complex expression macro can't parse + +**Solution**: Emit helpful error with workaround + +``` +error: Unable to parse seed expression. Supported patterns: + - Literals: b"seed", "seed" + - Constants: MY_SEED (uppercase) + - Account refs: account.key().as_ref() + - Params: params.field.as_ref(), params.field.to_le_bytes().as_ref() + - Functions: my_fn(&a.key(), &b.key()).as_ref() + +If your expression doesn't match, use explicit #[compressible(seeds = (...))] +override on the field. +``` + +### 4. params.\* Type Inference + +**Scenario**: Need to know type of `params.owner` for seeds struct + +**Solution**: Infer from data struct fields by name matching + +```rust +// Seeds: params.owner.as_ref() +// UserRecord has: owner: Pubkey +// Therefore: UserRecordSeeds.owner: Pubkey + +// Seeds: params.category_id.to_le_bytes().as_ref() +// UserRecord has: category_id: u64 +// Therefore: UserRecordSeeds.category_id: u64 +``` + +If no match found, default to `Pubkey` for `.as_ref()`, `u64` for `.to_le_bytes()`. + +### 5. Token Authority Resolution + +**Scenario**: Need authority seeds for CToken accounts + +**Solution**: Authority seeds are specified inline in the `#[compressible_token]` attribute: + +```rust +// Authority seeds are required and specified inline +#[account(mut, seeds = [b"vault", cmint.key().as_ref()], bump)] +#[compressible_token(Vault, authority = [b"vault_authority"])] +pub vault: UncheckedAccount<'info>, +``` + +The `authority = [...]` parameter specifies the seeds used to derive the CToken authority PDA +for signing during compression operations. + +--- + +## Migration Guide + +### Step 1: Update Module Level + +```rust +// REMOVE this: +#[compressible( + UserRecord = (seeds = ("user_record", ctx.authority, data.owner)), + owner = Pubkey, +)] + +// ADD this: +#[compressible_program] // No type list needed! +``` + +### Step 2: Ensure Anchor Seeds Are Defined + +Your `#[account(seeds = [...])]` attributes already contain the seeds - no changes needed there! + +### Step 3: Add #[compressible] to PDA Fields + +```rust +#[account(init, seeds = [...], bump)] +#[compressible] // Add this marker +pub user_record: Account<'info, UserRecord>, +``` + +### Step 4: For Tokens, Add Explicit Mapping with Authority + +```rust +#[account(mut, seeds = [...], bump)] +#[compressible_token(Vault, authority = [b"vault_authority"])] // Variant + authority seeds +pub vault: UncheckedAccount<'info>, +``` + +--- + +## Test Plan ✅ PASSED + +1. **Unit Tests**: Parse Anchor seeds correctly for all supported types ✅ +2. **Integration Tests**: Full create → compress → decompress cycle ✅ +3. **Edge Cases**: Box, AccountLoader, function call seeds ✅ +4. **Error Cases**: Missing types, conflicting seeds, unparseable expressions ✅ +5. **Migration**: Verified with csdk-anchor-full-derived-test ✅ + +--- + +## Implementation Order (ALL COMPLETE ✅) + +1. **Phase 1**: Add Anchor seed extraction to `anchor_seeds.rs` ✅ +2. **Phase 2**: Create file_scanner.rs to read external .rs files ✅ +3. **Phase 3**: Wire extracted seeds to codegen in `instructions.rs` ✅ +4. **Phase 4**: Add `#[compressible_program]` module-level macro ✅ +5. **Phase 5**: Generate all required code (enums, traits, decompress, compress) ✅ +6. **Phase 6**: Update csdk-anchor-full-derived-test to use new syntax ✅ +7. **Phase 7**: All tests passing! ✅ diff --git a/sdk-libs/macros/OPTION_A_PLAN.md b/sdk-libs/macros/OPTION_A_PLAN.md new file mode 100644 index 0000000000..2efa55adb4 --- /dev/null +++ b/sdk-libs/macros/OPTION_A_PLAN.md @@ -0,0 +1,404 @@ +# Option A Implementation Plan + +## Executive Summary + +Add `StandardAta` and `PackedStandardAta` as always-present variants in `CompressedAccountVariant` to enable decompression of arbitrary ATAs without program-specific enum variants. + +**Key insight**: The existing `CompressedMint` variant already handles standard mints. We only need to add support for standard ATAs. + +--- + +## Phase 1: Data Structures (ctoken-sdk/src/pack.rs) + +### 1.1 Add StandardAtaData struct + +```rust +/// Standard ATA data for decompression. +/// The wallet owner signs the transaction (not the program). +/// TokenData.owner = ATA address (derived from wallet + mint). +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct StandardAtaData { + /// Wallet owner pubkey - MUST be a signer on the transaction. + pub wallet: Pubkey, + /// Mint pubkey for this token account. + pub mint: Pubkey, + /// Token data from compressed account. + /// CRITICAL: token_data.owner = ATA address (not wallet). + pub token_data: TokenData, +} +``` + +### 1.2 Add PackedStandardAtaData struct + +```rust +/// Packed StandardAtaData with indices into remaining_accounts. +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct PackedStandardAtaData { + /// Index of wallet in remaining_accounts (must be signer). + pub wallet_index: u8, + /// Index of mint in remaining_accounts. + pub mint_index: u8, + /// Index of ATA address in remaining_accounts. + pub ata_index: u8, + /// Packed token data (owner/delegate/mint are indices). + pub token_data: InputTokenDataCompressible, +} +``` + +### 1.3 Implement Pack/Unpack traits + +```rust +impl Pack for StandardAtaData { + type Packed = PackedStandardAtaData; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + let (ata_address, _bump) = + crate::ctoken::get_associated_ctoken_address_and_bump(&self.wallet, &self.mint); + + // Insert wallet as signer + let wallet_index = remaining_accounts.insert_or_get_config(self.wallet, true, false); + let mint_index = remaining_accounts.insert_or_get(self.mint); + let ata_index = remaining_accounts.insert_or_get(ata_address); + + PackedStandardAtaData { + wallet_index, + mint_index, + ata_index, + token_data: self.token_data.pack(remaining_accounts), + } + } +} + +impl Unpack for PackedStandardAtaData { + type Unpacked = StandardAtaData; + + fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result { + let wallet = *remaining_accounts + .get(self.wallet_index as usize) + .ok_or(ProgramError::NotEnoughAccountKeys)? + .key; + let mint = *remaining_accounts + .get(self.mint_index as usize) + .ok_or(ProgramError::NotEnoughAccountKeys)? + .key; + let token_data = self.token_data.unpack(remaining_accounts)?; + + Ok(StandardAtaData { wallet, mint, token_data }) + } +} +``` + +--- + +## Phase 2: Macro Changes (sdk-libs/macros) + +### 2.1 variant_enum.rs - Add StandardAta variants + +Update `compressed_account_variant` to include StandardAta variants: + +```rust +let enum_def = quote! { + #[derive(Clone, Debug, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + pub enum CompressedAccountVariant { + #(#account_variants)* + PackedCTokenData(light_ctoken_sdk::compat::PackedCTokenData), + CTokenData(light_ctoken_sdk::compat::CTokenData), + CompressedMint(light_ctoken_sdk::compat::CompressedMintData), + // NEW: Standard ATA variants + StandardAta(light_ctoken_sdk::compat::StandardAtaData), + PackedStandardAta(light_ctoken_sdk::compat::PackedStandardAtaData), + } +}; +``` + +### 2.2 variant_enum.rs - Update trait implementations + +Add match arms for StandardAta in: +- `DataHasher` impl (unreachable for packed) +- `HasCompressionInfo` impl (unreachable - token accounts don't have compression_info) +- `Size` impl (unreachable) +- `Pack` impl (StandardAta -> PackedStandardAta) +- `Unpack` impl (PackedStandardAta -> StandardAta) + +### 2.3 decompress_context.rs - Update collect_all_accounts + +Add handling for StandardAta in `collect_all_accounts`: + +```rust +CompressedAccountVariant::PackedStandardAta(data) => { + // Standard ATAs are processed alongside program tokens + // They share the same token processing path with is_ata=true behavior + standard_ata_accounts.push((data, meta)); +} +CompressedAccountVariant::StandardAta(_) => { + unreachable!("Unpacked StandardAta should not appear in packed instruction data"); +} +``` + +### 2.4 instructions.rs - Update collect_all_accounts helper + +Modify `collect_all_accounts` to return standard ATAs as a fourth tuple element: + +```rust +fn collect_all_accounts<'a, 'b, 'info>( + // ... params ... +) -> Result<( + Vec, + Vec<(PackedCTokenData, Meta)>, + Vec<(CompressedMintData, Meta)>, + Vec<(PackedStandardAtaData, Meta)>, // NEW +), ProgramError> +``` + +--- + +## Phase 3: Runtime Changes (ctoken-sdk/src/compressible) + +### 3.1 decompress_runtime.rs - Process standard ATAs + +Add processing for standard ATAs in `process_decompress_tokens_runtime`: + +```rust +/// Process standard ATAs alongside program tokens. +/// Standard ATAs use the same Transfer2 CPI but: +/// 1. Don't require program-derived seeds +/// 2. Wallet must be a TX signer (validated here) +/// 3. ATA is derived from (wallet, ctoken_program, mint) +pub fn process_standard_atas_in_token_flow<'info>( + standard_atas: Vec<(PackedStandardAtaData, CompressedAccountMetaNoLamportsNoAddress)>, + packed_accounts: &[AccountInfo<'info>], + fee_payer: &AccountInfo<'info>, + ctoken_config: &AccountInfo<'info>, + ctoken_rent_sponsor: &AccountInfo<'info>, + cpi_accounts: &CpiAccounts<'_, 'info>, + token_decompress_indices: &mut Vec, +) -> Result<(), ProgramError> { + for (packed_ata, meta) in standard_atas { + let wallet_info = &packed_accounts[packed_ata.wallet_index as usize]; + let mint_info = &packed_accounts[packed_ata.mint_index as usize]; + let ata_info = &packed_accounts[packed_ata.ata_index as usize]; + + // 1. Verify wallet is signer + if !wallet_info.is_signer { + msg!("StandardAta wallet must be signer: {:?}", wallet_info.key); + return Err(ProgramError::MissingRequiredSignature); + } + + // 2. Verify ATA derivation + let (derived_ata, bump) = derive_ctoken_ata(wallet_info.key, mint_info.key); + if derived_ata != *ata_info.key { + msg!("ATA derivation mismatch"); + return Err(ProgramError::InvalidAccountData); + } + + // 3. Create ATA (idempotent) + CreateAssociatedCTokenAccountCpi { + payer: fee_payer.clone(), + associated_token_account: ata_info.clone(), + owner: wallet_info.clone(), + mint: mint_info.clone(), + system_program: cpi_accounts.system_program()?.clone(), + bump, + compressible: CompressibleParamsCpi { + compressible_config: ctoken_config.clone(), + rent_sponsor: ctoken_rent_sponsor.clone(), + system_program: cpi_accounts.system_program()?.clone(), + pre_pay_num_epochs: 2, + lamports_per_write: None, + compress_to_account_pubkey: None, + token_account_version: TokenDataVersion::ShaFlat, + compression_only: true, + }, + idempotent: true, + }.invoke()?; + + // 4. Build decompress indices with TLV for ATA + let wallet_account_index = packed_accounts + .iter() + .position(|a| *a.key == *wallet_info.key) + .ok_or(ProgramError::NotEnoughAccountKeys)? as u8; + + let tlv = vec![ExtensionInstructionData::CompressedOnly( + CompressedOnlyExtensionInstructionData { + delegated_amount: 0, + withheld_transfer_fee: 0, + is_frozen: false, + compression_index: 0, + is_ata: true, + bump, + owner_index: wallet_account_index, + }, + )]; + + let source = MultiInputTokenDataWithContext { + owner: packed_ata.token_data.owner, // ATA address index + amount: packed_ata.token_data.amount, + has_delegate: packed_ata.token_data.has_delegate, + delegate: packed_ata.token_data.delegate, + mint: packed_ata.token_data.mint, + version: packed_ata.token_data.version, + merkle_context: meta.tree_info.into(), + root_index: meta.tree_info.root_index, + }; + + token_decompress_indices.push(DecompressFullIndices { + source, + destination_index: packed_ata.ata_index, + tlv: Some(tlv), + is_ata: true, + }); + } + + Ok(()) +} +``` + +### 3.2 Update function signature + +Modify `process_decompress_tokens_runtime` to accept standard ATAs: + +```rust +pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( + // ... existing params ... + ctoken_accounts: Vec<(PackedCTokenData, Meta)>, + standard_ata_accounts: Vec<(PackedStandardAtaData, Meta)>, // NEW + // ... +) -> Result<(), ProgramError> +``` + +--- + +## Phase 4: SDK Runtime Changes (sdk/src/compressible) + +### 4.1 decompress_runtime.rs - Update DecompressContext trait + +Add method to handle standard ATAs in the trait: + +```rust +/// Returns standard ATA accounts for separate processing. +fn standard_ata_accounts(&self) -> Vec<(PackedStandardAtaData, CompressedMeta)> { + Vec::new() // Default: no standard ATAs +} +``` + +### 4.2 Update process_decompress_accounts_idempotent + +Pass standard ATAs to process_tokens: + +```rust +// After collect_all_accounts returns (pdas, tokens, mints, standard_atas) +ctx.process_tokens( + // ... existing params ... + compressed_token_accounts, + standard_ata_accounts, // NEW + // ... +)?; +``` + +--- + +## Phase 5: Client Changes (compressible-client/src/lib.rs) + +### 5.1 Add StandardAtaInput struct + +```rust +/// Input for standard ATA decompression (no program-specific variant needed) +pub struct StandardAtaInput { + /// Wallet owner - MUST sign the transaction + pub wallet: Pubkey, + /// Mint for the token + pub mint: Pubkey, + /// Token data from indexer (owner = ATA address) + pub token_data: TokenData, + /// Tree info for validity proof + pub tree_info: TreeInfo, +} +``` + +### 5.2 Update decompress_accounts_idempotent signature + +```rust +pub fn decompress_accounts_idempotent( + program_id: &Pubkey, + discriminator: &[u8], + decompressed_account_addresses: &[Pubkey], + compressed_accounts: &[(CompressedAccount, T)], + standard_atas: &[StandardAtaInput], // NEW + program_account_metas: &[AccountMeta], + validity_proof_with_context: ValidityProofWithContext, +) -> Result> +``` + +### 5.3 Pack standard ATAs in instruction builder + +```rust +// Pack standard ATAs +for ata_input in standard_atas { + let (ata_address, _) = derive_ctoken_ata(&ata_input.wallet, &ata_input.mint); + + // Insert wallet as signer + remaining_accounts.insert_or_get_config(ata_input.wallet, true, false); + remaining_accounts.insert_or_get(ata_input.mint); + remaining_accounts.insert_or_get(ata_address); + + let standard_ata = StandardAtaData { + wallet: ata_input.wallet, + mint: ata_input.mint, + token_data: ata_input.token_data.clone(), + }; + let packed = standard_ata.pack(&mut remaining_accounts); + + typed_compressed_accounts.push(CompressedAccountData { + meta: /* from validity_proof_with_context */, + data: CompressedAccountVariant::PackedStandardAta(packed), + }); +} +``` + +--- + +## Phase 6: Testing + +### 6.1 Update existing test + +Modify `test_create_pdas_and_mint_auto` to: +1. Use `StandardAtaInput` for user ATA decompression +2. Verify wallet signer requirement +3. Test mixed batch (PDAs + program tokens + standard ATAs) + +### 6.2 New test cases + +1. **Standard ATA only**: Decompress single standard ATA +2. **Mixed batch**: PDAs + CompressedMint + StandardAta + program token +3. **Signer validation**: Ensure non-signer wallet fails +4. **ATA derivation validation**: Ensure wrong wallet/mint combo fails + +--- + +## Implementation Order + +1. **ctoken-sdk/src/pack.rs** - Add data structures (30 min) +2. **macros/src/compressible/variant_enum.rs** - Add variants + trait impls (45 min) +3. **macros/src/compressible/decompress_context.rs** - Handle new variants (30 min) +4. **ctoken-sdk/src/compressible/decompress_runtime.rs** - Process standard ATAs (60 min) +5. **sdk/src/compressible/decompress_runtime.rs** - Update trait + processor (30 min) +6. **compressible-client/src/lib.rs** - Client helpers (45 min) +7. **Test updates** - Verify functionality (60 min) + +**Total estimated time: 5-6 hours** + +--- + +## Open Questions (Resolved) + +1. **Q: Should standard ATAs use a separate variant or share `PackedCTokenData`?** + **A: Separate variant (`PackedStandardAta`) for cleaner handling and explicit wallet index.** + +2. **Q: How does the client know which accounts are standard ATAs vs program tokens?** + **A: Client explicitly creates `StandardAtaInput` vs wrapping in program's `CTokenAccountVariant`.** + +3. **Q: Do standard ATAs require `cmint_authority` account?** + **A: No. Standard ATAs only need wallet signer. `cmint_authority` is only for mint decompression.** + +4. **Q: Can standard ATAs be mixed with program tokens in single instruction?** + **A: Yes. All tokens (standard + program) are batched into single Transfer2 CPI.** diff --git a/sdk-libs/macros/OPTION_A_STATE_FLOW.md b/sdk-libs/macros/OPTION_A_STATE_FLOW.md new file mode 100644 index 0000000000..0a0d62d1ec --- /dev/null +++ b/sdk-libs/macros/OPTION_A_STATE_FLOW.md @@ -0,0 +1,316 @@ +# Option A: Standard ATA/Mint Variants - State Flow Diagram + +## Overview + +Option A adds `StandardAta` and `PackedStandardAta` variants to the macro-generated `CompressedAccountVariant` enum, enabling unified decompression of arbitrary ATAs and mints alongside program-specific PDAs. + +--- + +## High-Level Decompression Flow + +``` + decompress_accounts_idempotent + | + v + +------------------------------------+ + | parse CompressedAccountData[] | + +------------------------------------+ + | + +---------------+---------------+ + | | | + v v v + +--------+ +---------+ +--------+ + | PDAs | | Tokens | | Mints | + +--------+ +---------+ +--------+ + | | | + v v v + +---------+ +---------------+ +--------+ + | CPI to | | process_tokens| | CPI to | + | Light | | _runtime | | ctoken | + | System | +---------------+ | mint | + +---------+ | +--------+ + | + +---------------------+---------------------+ + | | | + v v v + +-------------+ +-------------+ +-------------+ + | Program PDA | | StandardAta | | CompressedMint | + | Token (Vault)| | (UserAta) | | (CMint) | + +-------------+ +-------------+ +-------------+ + | | | + v v v + derive seeds derive_ctoken_ata find_cmint_address + from variant (wallet, mint) (mint_seed) + | | | + v v v + CreateCToken CreateAssociated DecompressCMint + AccountCpi CTokenAccountCpi Cpi + (invoke_signed) (invoke - wallet (invoke) + signs tx) +``` + +--- + +## Token Account Type Decision Tree + +``` + PackedCTokenData + | + v + +---------------------------+ + | V.is_ata() returns what? | + +---------------------------+ + | + +------------------+------------------+ + | | + is_ata = true is_ata = false + | | + v v + +----------------+ +------------------+ + | Standard ATA | | Program-owned | + | Derivation | | Token Account | + +----------------+ +------------------+ + | | + v v + derive_ctoken_ata(wallet, mint) get_seeds() from variant + wallet must be TX signer program signs via CPI + | | + v v + CreateAssociatedCTokenAccountCpi CreateCTokenAccountCpi + .invoke() - no program signer .invoke_signed(&[seeds]) +``` + +--- + +## StandardAta Detailed Flow + +``` + Client Side + ----------- + StandardAtaInput { + wallet: Pubkey, // must sign TX + mint: Pubkey, + token_data: TokenData, // owner = ATA address + tree_info: TreeInfo, + } + | + v + pack_standard_ata() + | + +---> remaining_accounts.insert_or_get_config(wallet, signer=true) + +---> remaining_accounts.insert_or_get(mint) + +---> derive_ctoken_ata(wallet, mint) -> ata_address + +---> remaining_accounts.insert_or_get(ata_address) + +---> pack token_data indices + | + v + PackedStandardAtaData { + wallet_index: u8, + mint_index: u8, + ata_index: u8, + token_data: InputTokenDataCompressible, + } + | + v + CompressedAccountData { + meta: CompressedAccountMetaNoLamportsNoAddress, + data: CompressedAccountVariant::PackedStandardAta(packed), + } + + + Runtime Side + ------------ + collect_all_accounts() + | + v + match CompressedAccountVariant::PackedStandardAta(packed) + | + v + Extract to standard_ata_accounts: Vec<(PackedStandardAtaData, Meta)> + | + v + process_decompress_tokens_runtime() + | + v + for (packed_ata, meta) in standard_atas { + | + v + // 1. Validate wallet is signer + packed_accounts[wallet_index].is_signer? -> MissingRequiredSignature + | + v + // 2. Verify ATA derivation + derive_ctoken_ata(wallet, mint) == packed_accounts[ata_index]? -> InvalidAccountData + | + v + // 3. Create ATA (idempotent) + CreateAssociatedCTokenAccountCpi { + owner: wallet, + mint: mint, + bump: derived_bump, + compressible: { + compression_only: true, // ATAs must be compression_only + ... + }, + idempotent: true, + }.invoke() // No signer - wallet signs TX + | + v + // 4. Build decompress indices with TLV + DecompressFullIndices { + source: MultiInputTokenDataWithContext { + owner: ata_index, // ATA address in merkle tree + ... + }, + destination_index: ata_index, + tlv: Some([CompressedOnly { is_ata: true, owner_index: wallet_index, bump }]), + is_ata: true, + } + } + | + v + // Single Transfer2 CPI for all tokens (program PDAs + standard ATAs) + decompress_full_ctoken_accounts_with_indices(...) +``` + +--- + +## CompressedAccountVariant Enum (After Option A) + +```rust +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub enum CompressedAccountVariant { + // === Program-specific PDA variants (macro-generated) === + UserRecord(UserRecord), + PackedUserRecord(PackedUserRecord), + GameSession(GameSession), + PackedGameSession(PackedGameSession), + + // === Token variants (macro-generated) === + PackedCTokenData(PackedCTokenData), + CTokenData(CTokenData), + + // === Mint variant (existing) === + CompressedMint(CompressedMintData), + + // === NEW: Standard ATA variants (always present) === + StandardAta(StandardAtaData), + PackedStandardAta(PackedStandardAtaData), +} +``` + +--- + +## CPI Context Batching (Multi-Type Decompression) + +``` + Execution Order: PDAs -> Mints -> Tokens (tokens always last) + + Case 1: Single Type (no CPI context) + ------------------------------------ + PDAs only: LightSystemProgramCpi.invoke() + Mints only: DecompressCMintCpi.invoke() + Tokens only: Transfer2 CPI invoke() + + Case 2: Multi-Type (with CPI context batching) + ---------------------------------------------- + + PDAs Mints Tokens CPI Context Action + ---- ----- ------ ------------------ + Yes No No execute directly (no context) + No Yes No execute directly (no context) + No No Yes execute directly (no context) + + Yes Yes No PDAs: first_set_context + Mints: execute (consume) + + Yes No Yes PDAs: first_set_context + Tokens: execute (consume) + + No Yes Yes Mints: first_set_context + Tokens: execute (consume) + + Yes Yes Yes PDAs: first_set_context + Mints: set_context + Tokens: execute (consume) +``` + +--- + +## Validation Rules + +### StandardAta Validation + +1. **Wallet signer check**: `remaining_accounts[wallet_index].is_signer == true` +2. **ATA derivation check**: `derive_ctoken_ata(wallet, mint) == remaining_accounts[ata_index].key` +3. **Owner consistency**: `token_data.owner` (index) points to ATA address (not wallet) + +### CompressedMint Validation + +1. **CMint derivation**: `find_cmint_address(mint_seed) == cmint_pda` +2. **Authority**: fee_payer must be mint authority OR explicit cmint_authority provided + +### Program Token (Vault) Validation + +1. **PDA derivation**: `get_seeds(variant, accounts)` returns matching PDA +2. **Authority derivation**: `get_authority_seeds(variant, accounts)` returns owner PDA +3. **Program signing**: invoke_signed with seed-derived bumps + +--- + +## Account Lookup Indices + +``` +remaining_accounts layout (after system accounts): ++--------------------------------------------------------------------+ +| idx | account | usage | ++--------------------------------------------------------------------+ +| 0 | output_queue | state tree output | +| 1 | state_tree | merkle tree | +| 2 | input_queue | nullifier queue | +| ... | tree accounts | from validity proof | +| n | wallet (signer) | StandardAta wallet owner | +| n+1 | mint | token mint | +| n+2 | ATA address | derived from wallet+mint | +| n+3 | vault_authority | program-owned token authority | +| n+4 | cmint_pda | CMint address | +| ... | other accounts | | +| end | decompressed PDAs/tokens | accounts being decompressed | ++--------------------------------------------------------------------+ + +PackedStandardAtaData { + wallet_index: n, // points to signer wallet + mint_index: n+1, // points to mint + ata_index: n+2, // points to derived ATA + token_data: { + owner: n+2, // ATA address (matches compressed token owner) + mint: n+1, + amount: ..., + ... + } +} +``` + +--- + +## Files Changed Summary + +``` +sdk-libs/ + macros/src/compressible/ + variant_enum.rs # Add StandardAta, PackedStandardAta variants + decompress_context.rs # Handle StandardAta in collect_all_accounts + instructions.rs # Update collect_all_accounts helper + + ctoken-sdk/src/ + pack.rs # Add StandardAtaData, PackedStandardAtaData + Pack/Unpack + compressible/ + decompress_runtime.rs # Process standard ATAs in token flow + mod.rs # Re-export new types + + compressible-client/src/ + lib.rs # Add StandardAtaInput, update decompress helper + + sdk/src/compressible/ + decompress_runtime.rs # Pass standard ATAs to process_tokens +``` diff --git a/sdk-libs/macros/OVERVIEW.md b/sdk-libs/macros/OVERVIEW.md new file mode 100644 index 0000000000..ef762598ec --- /dev/null +++ b/sdk-libs/macros/OVERVIEW.md @@ -0,0 +1,194 @@ +# `#[compressible]` Macro Usage Guide + +## Supported Account Types + +| Type | Description | +| ------------------------- | ----------------------------------------------------- | +| **PDAs** | Program Derived Accounts with custom seeds | +| **Program-owned CTokens** | Token accounts owned by a program PDA (vault pattern) | + +--- + +## Program-Side: Macro Syntax + +```rust +#[compressible( + // PDA: TypeName = (seeds = (...)) + UserRecord = (seeds = ("user_record", ctx.authority, data.owner)), + + // Token: TypeName = (is_token, seeds = (...), authority = (...)) + Vault = (is_token, seeds = ("vault", ctx.mint), authority = ("vault_authority")), + + // Instruction data fields used in seeds + owner = Pubkey, +)] +#[program] +pub mod my_program { ... } +``` + +### Seed Components + +| Syntax | Description | +| ---------------- | ---------------------------------------- | +| `seeds = (...)` | Required. Tuple of seed elements | +| `"literal"` | Static seed bytes (string literal) | +| `b"literal"` | Static seed bytes (byte string literal) | +| `CONST` | Crate-level constant (`&str` or `&[u8]`) | +| `ctx.account` | Account from instruction context | +| `data.field` | Field from instruction data | +| `is_token` | Marks account as CToken (not PDA) | +| `authority = ()` | (tokens only) PDA that owns the token | + +**Constants:** Uppercase identifiers are resolved as `crate::CONST` and support both `&str` and `&[u8]`: + +```rust +pub const MY_SEED: &str = "my_seed"; // &str constant +pub const MY_BYTES: &[u8] = b"my_bytes"; // &[u8] constant + +#[compressible( + MyAccount = (seeds = (MY_SEED, ctx.user)), + MyOther = (seeds = (MY_BYTES, ctx.user)), +)] +``` + +--- + +## Generated Code + +The macro generates: + +1. **`CompressedAccountVariant`** - enum with all PDA types + token variants +2. **`CTokenAccountVariant`** - enum for token account types +3. **`DecompressAccountsIdempotent`** - Anchor accounts struct +4. **`CompressAccountsIdempotent`** - Anchor accounts struct +5. **`SeedParams`** - struct for `data.*` seed fields +6. **`CTokenSeedProvider`** impl - derives token seeds +7. **`PdaSeedDerivation`** impl - derives PDA seeds +8. **`DecompressContext`** impl - runtime decompression logic +9. **`decompress_accounts_idempotent()`** - instruction handler +10. **`compress_accounts_idempotent()`** - instruction handler + +--- + +## Client-Side Usage + +### Building Decompress Instruction + +```rust +use light_compressible_client::compressible_instruction; + +// 1. Fetch compressed accounts from indexer +let compressed_user = indexer.get_compressed_account(user_hash).await?; +let compressed_vault = indexer.get_compressed_account(vault_hash).await?; + +// 2. Get validity proof +let proof = indexer.get_validity_proof( + vec![compressed_user.hash, compressed_vault.hash], + vec![], + None, +).await?; + +// 3. Build instruction +let instruction = compressible_instruction::decompress_accounts_idempotent( + &program_id, + &DECOMPRESS_DISCRIMINATOR, + &[user_pda, vault_pda], // Target on-chain addresses + &[ + (compressed_user, user_data), // PDAs first + (compressed_vault, token_data), // Tokens after + ], + &program_accounts.to_account_metas(None), + proof, +)?; + +// 4. Append SeedParams if needed +let seed_params = SeedParams { owner }; +instruction.data.extend_from_slice(&borsh::to_vec(&seed_params)?); +``` + +### Account Ordering + +When mixing PDAs and tokens, order matters for CPI context: + +```rust +// Correct: PDAs first, tokens after +&[ + (compressed_pda, pda_data), + (compressed_token, token_data), +] +``` + +--- + +## CPI Context Rules + +When decompressing **both PDAs and tokens** in one instruction: + +1. PDAs **write** to CPI context first +2. Tokens **execute** (consume CPI context) last +3. CPI context validation checks: `cpi_context.associated_tree == first_input.tree` **at execution time** + +**Critical:** The client uses the **first token's** `cpi_context`, not the first PDA's: + +```rust +// In compressible-client (already handled internally): +// Uses first TOKEN's tree context since tokens execute last +let first_token_cpi_context = compressed_accounts + .iter() + .find(|(acc, _)| acc.owner == C_TOKEN_PROGRAM_ID) + .map(|(acc, _)| acc.tree_info.cpi_context.unwrap()); +``` + +--- + +## Example: Full Program + +```rust +use anchor_lang::prelude::*; +use light_sdk_macros::compressible; + +/// Seed constants - both &str and &[u8] are supported +pub const PROFILE_SEED: &str = "profile"; +pub const VAULT_SEED: &[u8] = b"vault"; + +#[compressible( + // PDA with &str constant + UserProfile = (seeds = (PROFILE_SEED, ctx.authority, data.user_id)), + + // Token with &[u8] constant + UserVault = (is_token, seeds = (VAULT_SEED, ctx.mint), authority = ("vault_auth", ctx.authority)), + + // Seed params + user_id = [u8; 32], +)] +#[program] +pub mod my_program { + use super::*; + + pub fn create_profile(ctx: Context, user_id: [u8; 32]) -> Result<()> { + // ... create compressed profile + Ok(()) + } + + // decompress_accounts_idempotent is auto-generated + // compress_accounts_idempotent is auto-generated +} + +#[derive(Accounts)] +pub struct CreateProfile<'info> { + #[account(mut)] + pub authority: Signer<'info>, + pub mint: Account<'info, Mint>, +} +``` + +--- + +## Key Files + +| File | Purpose | +| -------------------------------- | ------------------------------- | +| `macros/src/compressible/` | Macro implementation | +| `sdk/src/compressible/` | Runtime traits & PDA processing | +| `ctoken-sdk/src/compressible/` | Token decompression runtime | +| `compressible-client/src/lib.rs` | Client instruction builders | diff --git a/sdk-libs/macros/SPEC_OPTION_A.md b/sdk-libs/macros/SPEC_OPTION_A.md new file mode 100644 index 0000000000..56309fb4d0 --- /dev/null +++ b/sdk-libs/macros/SPEC_OPTION_A.md @@ -0,0 +1,547 @@ +# SPEC: Option A - Standard Variants in Macro-Generated Enum + +## Overview + +Add `StandardAta` and `StandardMint` as always-present variants in the macro-generated `CTokenAccountVariant` enum. Programs automatically get these standard variants without declaration. + +## Goals + +1. Enable decompression of arbitrary ATAs and Mints without per-program customization +2. Use fixed, known data structures and derivation logic +3. Maintain single unified enum for all account types +4. Zero breaking changes to existing programs that don't use standard types + +--- + +## Data Structures + +### StandardAtaData (New) + +```rust +/// Standard ATA data for decompression. +/// Compressed TokenData.owner = ATA address (NOT wallet). +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct StandardAtaData { + /// Wallet owner pubkey - MUST be a signer on the transaction. + /// The ATA is derived from (wallet, ctoken_program_id, mint). + pub wallet: Pubkey, + /// Mint pubkey for this token account. + pub mint: Pubkey, + /// Token data from compressed account. + /// CRITICAL: token_data.owner = ATA address (not wallet). + pub token_data: TokenData, +} +``` + +### PackedStandardAtaData (New) + +```rust +/// Packed StandardAtaData with indices into remaining_accounts. +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct PackedStandardAtaData { + /// Index of wallet in remaining_accounts (must be signer). + pub wallet_index: u8, + /// Index of mint in remaining_accounts. + pub mint_index: u8, + /// Index of ATA address in remaining_accounts (same as token_data.owner). + pub ata_index: u8, + /// Packed token data (owner/delegate/mint are indices). + pub token_data: InputTokenDataCompressible, +} +``` + +### StandardMintData (Existing CompressedMintData - Reuse) + +```rust +/// Already exists in ctoken-sdk/src/pack.rs as CompressedMintData. +/// Rename/alias to StandardMintData for clarity. +pub type StandardMintData = CompressedMintData; + +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct CompressedMintData { + /// Mint seed pubkey (used to derive CMint PDA via find_cmint_address). + pub mint_seed_pubkey: Pubkey, + /// Compressed mint with context (from indexer). + pub compressed_mint_with_context: CompressedMintWithContext, + /// Rent payment in epochs (must be >= 2). + pub rent_payment: u8, + /// Lamports for future write operations. + pub write_top_up: u32, +} +``` + +--- + +## Enum Changes + +### CTokenAccountVariant (Modified) + +The macro will always generate these standard variants: + +```rust +// sdk-libs/macros/src/compressible/seed_providers.rs +pub fn generate_ctoken_account_variant_enum(specs: &[TokenSeedSpec]) -> Result { + // ... existing program-specific variants ... + + quote! { + #[derive(Clone, Copy, Debug, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] + pub enum CTokenAccountVariant { + // Program-specific variants (from macro args) + #(#program_variants,)* + + // Standard variants (always present) + /// Standard ATA - uses fixed derivation (wallet, ctoken_program, mint). + StandardAta, + /// Standard Mint - uses fixed derivation find_cmint_address(mint_seed). + StandardMint, + } + } +} +``` + +### CompressedAccountVariant (Modified) + +```rust +// sdk-libs/macros/src/compressible/variant_enum.rs +pub enum CompressedAccountVariant { + // Program PDA variants + #(#account_variants)* + + // Token variants + PackedCTokenData(PackedCTokenData), + CTokenData(CTokenData), + + // Mint variant (existing) + CompressedMint(CompressedMintData), + + // NEW: Standard ATA variant (separate from CTokenData for cleaner handling) + StandardAta(StandardAtaData), + PackedStandardAta(PackedStandardAtaData), +} +``` + +--- + +## Trait Implementations + +### CTokenSeedProvider for StandardAta + +```rust +impl CTokenSeedProvider for CTokenAccountVariant { + // ... existing match arms ... + + fn get_seeds<'a, 'info>( + &self, + accounts: &'a Self::Accounts<'info>, + remaining_accounts: &'a [AccountInfo<'info>], + ) -> Result<(Vec>, Pubkey), ProgramError> { + match self { + // ... existing program-specific arms ... + + CTokenAccountVariant::StandardAta => { + // StandardAta doesn't use program seeds - derivation is fixed. + // Return empty seeds; the runtime handles ATA creation separately. + Err(ProgramError::InvalidArgument) // Should not be called + } + CTokenAccountVariant::StandardMint => { + // StandardMint doesn't use program seeds - derivation is fixed. + Err(ProgramError::InvalidArgument) // Should not be called + } + } + } + + fn get_authority_seeds<'a, 'info>(...) -> Result<...> { + match self { + CTokenAccountVariant::StandardAta => { + Err(ProgramError::InvalidArgument) // ATAs don't need authority seeds + } + CTokenAccountVariant::StandardMint => { + Err(ProgramError::InvalidArgument) // Mints don't need authority seeds for decompress + } + // ... existing arms ... + } + } + + fn is_ata(&self) -> bool { + matches!(self, CTokenAccountVariant::StandardAta) + } +} +``` + +### HasTokenVariant Updates + +```rust +impl HasTokenVariant for CompressedAccountData { + fn is_packed_ctoken(&self) -> bool { + matches!( + self.data, + CompressedAccountVariant::PackedCTokenData(_) + | CompressedAccountVariant::PackedStandardAta(_) + ) + } + + fn is_compressed_mint(&self) -> bool { + matches!(self.data, CompressedAccountVariant::CompressedMint(_)) + } + + fn is_standard_ata(&self) -> bool { + matches!( + self.data, + CompressedAccountVariant::StandardAta(_) + | CompressedAccountVariant::PackedStandardAta(_) + ) + } +} +``` + +### Pack/Unpack for StandardAtaData + +```rust +impl Pack for StandardAtaData { + type Packed = PackedStandardAtaData; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + // Derive ATA address from wallet + mint + let (ata_address, _bump) = derive_ctoken_ata(&self.wallet, &self.mint); + + // Insert all required accounts + let wallet_index = remaining_accounts.insert_or_get_config(self.wallet, true, false); // signer + let mint_index = remaining_accounts.insert_or_get(self.mint); + let ata_index = remaining_accounts.insert_or_get(ata_address); + + // Pack token data + let token_data = self.token_data.pack(remaining_accounts); + + PackedStandardAtaData { + wallet_index, + mint_index, + ata_index, + token_data, + } + } +} + +impl Unpack for PackedStandardAtaData { + type Unpacked = StandardAtaData; + + fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result { + let wallet = *remaining_accounts + .get(self.wallet_index as usize) + .ok_or(ProgramError::NotEnoughAccountKeys)? + .key; + let mint = *remaining_accounts + .get(self.mint_index as usize) + .ok_or(ProgramError::NotEnoughAccountKeys)? + .key; + let token_data = self.token_data.unpack(remaining_accounts)?; + + Ok(StandardAtaData { wallet, mint, token_data }) + } +} +``` + +--- + +## Runtime Processing + +### process_decompress_tokens_runtime (Modified) + +```rust +// sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs + +pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( + // ... existing params ... + // ADD: standard ATAs + standard_atas: Vec<(PackedStandardAtaData, CompressedAccountMetaNoLamportsNoAddress)>, +) -> Result<(), ProgramError> { + // ... existing token processing ... + + // Process standard ATAs + for (packed_ata, meta) in standard_atas.into_iter() { + let wallet_info = &packed_accounts[packed_ata.wallet_index as usize]; + let mint_info = &packed_accounts[packed_ata.mint_index as usize]; + let ata_info = &packed_accounts[packed_ata.ata_index as usize]; + + // Verify wallet is signer + if !wallet_info.is_signer { + msg!("StandardAta wallet must be signer: {:?}", wallet_info.key); + return Err(ProgramError::MissingRequiredSignature); + } + + // Verify ATA derivation + let (derived_ata, bump) = derive_ctoken_ata(wallet_info.key, mint_info.key); + if derived_ata != *ata_info.key { + msg!("ATA derivation mismatch: derived={:?}, provided={:?}", derived_ata, ata_info.key); + return Err(ProgramError::InvalidAccountData); + } + + // Create ATA if needed (idempotent) + CreateAssociatedCTokenAccountCpi { + payer: fee_payer.clone(), + associated_token_account: ata_info.clone(), + owner: wallet_info.clone(), + mint: mint_info.clone(), + system_program: cpi_accounts.system_program()?.clone(), + bump, + compressible: CompressibleParamsCpi { + compressible_config: ctoken_config.clone(), + rent_sponsor: ctoken_rent_sponsor.clone(), + system_program: cpi_accounts.system_program()?.clone(), + pre_pay_num_epochs: 2, + lamports_per_write: None, + compress_to_account_pubkey: None, + token_account_version: TokenDataVersion::ShaFlat, + compression_only: true, // ATAs require compression_only + }, + idempotent: true, + }.invoke()?; + + // Build decompress indices + let owner_index = packed_ata.token_data.owner; // ATA address index + let wallet_account_index = packed_ata.wallet_index; + + let source = MultiInputTokenDataWithContext { + owner: owner_index, + amount: packed_ata.token_data.amount, + has_delegate: packed_ata.token_data.has_delegate, + delegate: packed_ata.token_data.delegate, + mint: packed_ata.token_data.mint, + version: packed_ata.token_data.version, + merkle_context: meta.tree_info.into(), + root_index: meta.tree_info.root_index, + }; + + let tlv = vec![ExtensionInstructionData::CompressedOnly( + CompressedOnlyExtensionInstructionData { + delegated_amount: 0, + withheld_transfer_fee: 0, + is_frozen: false, + compression_index: 0, + is_ata: true, + bump, + owner_index: wallet_account_index, + }, + )]; + + let decompress_index = DecompressFullIndices { + source, + destination_index: packed_ata.ata_index, + tlv: Some(tlv), + is_ata: true, + }; + token_decompress_indices.push(decompress_index); + } + + // ... rest of existing logic (single Transfer2 CPI) ... +} +``` + +--- + +## collect_all_accounts (Modified) + +```rust +// Macro-generated in __macro_helpers module + +fn collect_all_accounts<'a, 'b, 'info>( + // ... existing params ... +) -> Result<( + Vec, // PDAs + Vec<(PackedCTokenData, Meta)>, // Program tokens + Vec<(CompressedMintData, Meta)>, // Mints + Vec<(PackedStandardAtaData, Meta)>, // NEW: Standard ATAs +), ProgramError> { + // ... existing setup ... + + let mut standard_ata_accounts = Vec::new(); + + for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { + let meta = compressed_data.meta; + match compressed_data.data { + // ... existing PDA arms ... + + CompressedAccountVariant::PackedCTokenData(data) => { + compressed_token_accounts.push((data, meta)); + } + CompressedAccountVariant::CompressedMint(data) => { + compressed_mint_accounts.push((data, meta)); + } + + // NEW: Standard ATA handling + CompressedAccountVariant::PackedStandardAta(data) => { + standard_ata_accounts.push((data, meta)); + } + CompressedAccountVariant::StandardAta(_) => { + unreachable!("Unpacked StandardAta should not appear"); + } + + // ... other arms ... + } + } + + Ok((compressed_pda_infos, compressed_token_accounts, compressed_mint_accounts, standard_ata_accounts)) +} +``` + +--- + +## Client-Side Changes + +### compressible_instruction::decompress_accounts_idempotent (Modified) + +```rust +// sdk-libs/compressible-client/src/lib.rs + +pub fn decompress_accounts_idempotent( + program_id: &Pubkey, + discriminator: &[u8], + decompressed_account_addresses: &[Pubkey], + compressed_accounts: &[(CompressedAccount, T)], + // NEW: Standard ATAs + standard_atas: &[StandardAtaInput], + // NEW: Standard Mints + standard_mints: &[StandardMintInput], + program_account_metas: &[AccountMeta], + validity_proof_with_context: ValidityProofWithContext, +) -> Result> +where + T: Pack + Clone + std::fmt::Debug, +{ + // ... existing setup ... + + // Pack standard ATAs + for ata_input in standard_atas { + let (ata_address, _) = derive_ctoken_ata(&ata_input.wallet, &ata_input.mint); + remaining_accounts.insert_or_get_config(ata_input.wallet, true, false); // signer + remaining_accounts.insert_or_get(ata_input.mint); + remaining_accounts.insert_or_get(ata_address); + + // Build StandardAtaData and pack + let standard_ata = StandardAtaData { + wallet: ata_input.wallet, + mint: ata_input.mint, + token_data: ata_input.token_data.clone(), + }; + let packed = standard_ata.pack(&mut remaining_accounts); + + typed_compressed_accounts.push(CompressedAccountData { + meta: /* from validity_proof_with_context */, + data: CompressedAccountVariant::PackedStandardAta(packed), + }); + } + + // Pack standard mints + for mint_input in standard_mints { + let (cmint_address, _) = find_cmint_address(&mint_input.mint_seed); + remaining_accounts.insert_or_get(mint_input.mint_seed); + remaining_accounts.insert_or_get(cmint_address); + + typed_compressed_accounts.push(CompressedAccountData { + meta: /* from validity_proof_with_context */, + data: CompressedAccountVariant::CompressedMint(CompressedMintData { + mint_seed_pubkey: mint_input.mint_seed, + compressed_mint_with_context: mint_input.compressed_mint_with_context.clone(), + rent_payment: mint_input.rent_payment, + write_top_up: mint_input.write_top_up, + }), + }); + } + + // ... rest of instruction building ... +} + +/// Input for standard ATA decompression +pub struct StandardAtaInput { + pub wallet: Pubkey, // Must be tx signer + pub mint: Pubkey, + pub token_data: TokenData, // owner = ATA address + pub tree_info: TreeInfo, +} + +/// Input for standard mint decompression +pub struct StandardMintInput { + pub mint_seed: Pubkey, + pub compressed_mint_with_context: CompressedMintWithContext, + pub rent_payment: u8, + pub write_top_up: u32, + pub tree_info: TreeInfo, +} +``` + +--- + +## DecompressAccountsIdempotent Accounts (Modified) + +```rust +// Macro-generated accounts struct + +#[derive(Accounts)] +pub struct DecompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// Program's compressible config + pub config: AccountInfo<'info>, + + /// Program's rent sponsor + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + + // CToken accounts - REQUIRED if any tokens/ATAs/mints present + /// CToken rent sponsor (ctoken program's rent sponsor PDA) + #[account(mut)] + pub ctoken_rent_sponsor: Option>, + + /// CToken compressible config + pub ctoken_config: Option>, + + /// CToken program + pub ctoken_program: Option>, + + /// CToken CPI authority + pub ctoken_cpi_authority: Option>, + + // ... other optional accounts for program-specific seeds ... +} +``` + +The runtime will validate that ctoken accounts are Some when standard ATAs/mints are present. + +--- + +## Validation Rules + +1. **Standard ATA validation:** + - `wallet` must be a signer in the transaction + - `derive_ctoken_ata(wallet, mint)` must equal the ATA destination account + - `token_data.owner` (ATA address) must match derived ATA + +2. **Standard Mint validation:** + - `find_cmint_address(mint_seed)` must equal the CMint destination account + - No signature required (mint authority doesn't need to sign for decompress) + +3. **Account requirements:** + - If any standard ATAs or mints present: ctoken_config, ctoken_rent_sponsor, ctoken_program, ctoken_cpi_authority must be Some + - If only PDAs: ctoken accounts can be None + +--- + +## Files to Modify + +1. `sdk-libs/macros/src/compressible/seed_providers.rs` - Add StandardAta/StandardMint variants +2. `sdk-libs/macros/src/compressible/variant_enum.rs` - Add StandardAta variant handling +3. `sdk-libs/macros/src/compressible/instructions.rs` - Update collect_all_accounts +4. `sdk-libs/macros/src/compressible/decompress_context.rs` - Update trait impl +5. `sdk-libs/ctoken-sdk/src/pack.rs` - Add StandardAtaData, PackedStandardAtaData +6. `sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs` - Handle standard ATAs +7. `sdk-libs/sdk/src/compressible/decompress_runtime.rs` - Update process_decompress_accounts_idempotent +8. `sdk-libs/compressible-client/src/lib.rs` - Add standard ATA/mint params + +--- + +## Migration Path + +1. Existing programs: No changes required, StandardAta/StandardMint variants available automatically +2. New programs: Can use standard types without declaring in macro +3. Tests: Update to use new client helper signature + diff --git a/sdk-libs/macros/SPEC_OPTION_B.md b/sdk-libs/macros/SPEC_OPTION_B.md new file mode 100644 index 0000000000..e9f52ff165 --- /dev/null +++ b/sdk-libs/macros/SPEC_OPTION_B.md @@ -0,0 +1,797 @@ +# SPEC: Option B - Separate Standard Types from Instruction Data + +## Overview + +Introduce separate instruction data fields for standard ATAs and Mints, completely decoupled from the program-specific `CompressedAccountVariant` enum. Clean architectural separation. + +## Goals + +1. Complete decoupling of standard types from program enum +2. Client can pass any number of ATAs/Mints without knowing program internals +3. Clean, auditable separation of concerns +4. Fully standardized handling with no program-specific code paths + +--- + +## Instruction Data Format (Breaking Change) + +### Current Format + +```rust +pub struct DecompressMultipleAccountsIdempotentData { + pub proof: ValidityProof, + pub compressed_accounts: Vec>, + pub system_accounts_offset: u8, +} +``` + +### New Format + +```rust +/// New instruction data format with separate fields for standard types. +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct DecompressAccountsIdempotentData { + /// Validity proof covering ALL accounts (PDAs + ATAs + Mints). + pub proof: ValidityProof, + + /// Program-specific compressed accounts (PDAs and program-owned tokens). + pub compressed_accounts: Vec>, + + /// Standard ATAs - fixed derivation, wallet signs. + pub standard_atas: Vec, + + /// Standard Mints - fixed derivation, no signature required. + pub standard_mints: Vec, + + /// Offset to system accounts in remaining_accounts. + pub system_accounts_offset: u8, +} +``` + +--- + +## Data Structures + +### StandardAtaData (Client-Side) + +```rust +/// Standard ATA data for client-side instruction building. +/// Location: sdk-libs/compressible-client/src/types.rs (NEW FILE) +#[derive(Clone, Debug)] +pub struct StandardAtaData { + /// Wallet owner - MUST be transaction signer. + pub wallet: Pubkey, + /// Mint pubkey. + pub mint: Pubkey, + /// Token data from indexer. CRITICAL: token_data.owner = ATA address. + pub token_data: TokenData, + /// Tree info from indexer. + pub tree_info: TreeInfo, +} +``` + +### PackedStandardAtaData (Serialized) + +```rust +/// Packed StandardAta for on-chain deserialization. +/// Location: sdk-libs/sdk/src/compressible/standard_types.rs (NEW FILE) +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct PackedStandardAtaData { + /// Index of wallet in remaining_accounts (must be signer). + pub wallet_index: u8, + /// Index of mint in remaining_accounts. + pub mint_index: u8, + /// Index of ATA destination in remaining_accounts. + pub ata_destination_index: u8, + /// Packed token data (indices into remaining_accounts). + pub token_data: PackedTokenData, + /// Compressed account metadata. + pub meta: CompressedAccountMetaNoLamportsNoAddress, +} + +/// Minimal packed token data for standard ATAs. +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct PackedTokenData { + /// Index of owner (ATA address) in remaining_accounts. + pub owner_index: u8, + /// Index of mint in remaining_accounts. + pub mint_index: u8, + /// Token amount. + pub amount: u64, + /// Has delegate flag. + pub has_delegate: bool, + /// Delegate index (0 if no delegate). + pub delegate_index: u8, + /// Token data version (3 = ShaFlat). + pub version: u8, +} +``` + +### StandardMintData (Client-Side) + +```rust +/// Standard Mint data for client-side instruction building. +#[derive(Clone, Debug)] +pub struct StandardMintData { + /// Mint seed pubkey (derives CMint via find_cmint_address). + pub mint_seed: Pubkey, + /// Compressed mint with context from indexer. + pub compressed_mint_with_context: CompressedMintWithContext, + /// Rent payment in epochs (>= 2). + pub rent_payment: u8, + /// Lamports for future writes. + pub write_top_up: u32, + /// Tree info from indexer. + pub tree_info: TreeInfo, +} +``` + +### PackedStandardMintData (Serialized) + +```rust +/// Packed StandardMint for on-chain deserialization. +#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] +pub struct PackedStandardMintData { + /// Index of mint_seed in remaining_accounts. + pub mint_seed_index: u8, + /// Index of CMint destination in remaining_accounts. + pub cmint_destination_index: u8, + /// Compressed mint with context. + pub compressed_mint_with_context: CompressedMintWithContext, + /// Rent payment in epochs. + pub rent_payment: u8, + /// Write top-up lamports. + pub write_top_up: u32, + /// Compressed account metadata. + pub meta: CompressedAccountMetaNoLamportsNoAddress, +} +``` + +--- + +## Runtime Processing + +### process_decompress_accounts_idempotent (Modified) + +```rust +// sdk-libs/sdk/src/compressible/decompress_runtime.rs + +#[inline(never)] +#[allow(clippy::too_many_arguments)] +pub fn process_decompress_accounts_idempotent<'info, Ctx>( + ctx: &Ctx, + remaining_accounts: &[AccountInfo<'info>], + compressed_accounts: Vec, + standard_atas: Vec, // NEW + standard_mints: Vec, // NEW + proof: ValidityProof, + system_accounts_offset: u8, + cpi_signer: CpiSigner, + program_id: &Pubkey, + seed_params: Option<&Ctx::SeedParams>, +) -> Result<(), ProgramError> +where + Ctx: DecompressContext<'info>, +{ + // Determine what types we have + let has_program_accounts = !compressed_accounts.is_empty(); + let has_standard_atas = !standard_atas.is_empty(); + let has_standard_mints = !standard_mints.is_empty(); + + // Check ctoken accounts required + if (has_standard_atas || has_standard_mints) { + // Validate ctoken accounts are present + ctx.ctoken_config().ok_or_else(|| { + msg!("ctoken_config required for standard ATAs/Mints"); + ProgramError::NotEnoughAccountKeys + })?; + ctx.ctoken_rent_sponsor().ok_or_else(|| { + msg!("ctoken_rent_sponsor required for standard ATAs/Mints"); + ProgramError::NotEnoughAccountKeys + })?; + } + + // Count types for CPI context batching + let (has_tokens, has_pdas, has_mints) = check_account_types(&compressed_accounts); + let has_any_tokens = has_tokens || has_standard_atas; + let has_any_mints = has_mints || has_standard_mints; + + let type_count = has_any_tokens as u8 + has_pdas as u8 + has_any_mints as u8; + let needs_cpi_context = type_count >= 2; + + // ... setup CPI accounts ... + + // 1. Process PDAs (if any) - from compressed_accounts + let (compressed_pda_infos, compressed_token_accounts, program_mint_accounts) = + ctx.collect_all_accounts(...)?; + + if !compressed_pda_infos.is_empty() { + // ... existing PDA processing with CPI context ... + } + + // 2. Process Mints (standard + program-specific) + let all_mints: Vec<_> = standard_mints + .into_iter() + .map(|m| (m.into_compressed_mint_data(), m.meta)) + .chain(program_mint_accounts) + .collect(); + + if !all_mints.is_empty() { + process_all_mints( + ctx, + &cpi_accounts, + all_mints, + proof, + has_pdas, // has_prior_context + has_any_tokens, // has_subsequent + )?; + } + + // 3. Process Tokens (standard ATAs + program-specific) + if has_any_tokens { + process_all_tokens( + ctx, + remaining_accounts, + compressed_token_accounts, // program-specific + standard_atas, // standard ATAs + proof, + &cpi_accounts, + has_pdas || has_any_mints, // has_prior_context + program_id, + )?; + } + + Ok(()) +} +``` + +### process_standard_atas (New Function) + +```rust +// sdk-libs/ctoken-sdk/src/compressible/standard_ata.rs (NEW FILE) + +/// Process standard ATAs in unified flow. +/// Handles ATA creation (idempotent) and builds decompress indices. +#[inline(never)] +pub fn process_standard_atas<'info>( + standard_atas: Vec, + packed_accounts: &[AccountInfo<'info>], + fee_payer: &AccountInfo<'info>, + ctoken_config: &AccountInfo<'info>, + ctoken_rent_sponsor: &AccountInfo<'info>, + system_program: &AccountInfo<'info>, + decompress_indices: &mut Vec, +) -> Result<(), ProgramError> { + for packed_ata in standard_atas { + // Get accounts from indices + let wallet_info = packed_accounts + .get(packed_ata.wallet_index as usize) + .ok_or(ProgramError::NotEnoughAccountKeys)?; + let mint_info = packed_accounts + .get(packed_ata.mint_index as usize) + .ok_or(ProgramError::NotEnoughAccountKeys)?; + let ata_info = packed_accounts + .get(packed_ata.ata_destination_index as usize) + .ok_or(ProgramError::NotEnoughAccountKeys)?; + + // CRITICAL: Verify wallet is signer + if !wallet_info.is_signer { + msg!("StandardAta: wallet must be signer: {:?}", wallet_info.key); + return Err(ProgramError::MissingRequiredSignature); + } + + // Derive and verify ATA address + let (derived_ata, bump) = derive_ctoken_ata(wallet_info.key, mint_info.key); + if derived_ata != *ata_info.key { + msg!( + "StandardAta: derivation mismatch. wallet={:?}, mint={:?}, expected={:?}, got={:?}", + wallet_info.key, mint_info.key, derived_ata, ata_info.key + ); + return Err(ProgramError::InvalidAccountData); + } + + // Verify token_data.owner matches ATA address + let owner_info = packed_accounts + .get(packed_ata.token_data.owner_index as usize) + .ok_or(ProgramError::NotEnoughAccountKeys)?; + if *owner_info.key != derived_ata { + msg!( + "StandardAta: token_data.owner must equal ATA address. owner={:?}, ata={:?}", + owner_info.key, derived_ata + ); + return Err(ProgramError::InvalidAccountData); + } + + // Create ATA (idempotent) + CreateAssociatedCTokenAccountCpi { + payer: fee_payer.clone(), + associated_token_account: ata_info.clone(), + owner: wallet_info.clone(), + mint: mint_info.clone(), + system_program: system_program.clone(), + bump, + compressible: CompressibleParamsCpi { + compressible_config: ctoken_config.clone(), + rent_sponsor: ctoken_rent_sponsor.clone(), + system_program: system_program.clone(), + pre_pay_num_epochs: 2, + lamports_per_write: None, + compress_to_account_pubkey: None, + token_account_version: TokenDataVersion::ShaFlat, + compression_only: true, + }, + idempotent: true, + }.invoke()?; + + // Build decompress indices + let source = MultiInputTokenDataWithContext { + owner: packed_ata.token_data.owner_index, + amount: packed_ata.token_data.amount, + has_delegate: packed_ata.token_data.has_delegate, + delegate: packed_ata.token_data.delegate_index, + mint: packed_ata.token_data.mint_index, + version: packed_ata.token_data.version, + merkle_context: packed_ata.meta.tree_info.into(), + root_index: packed_ata.meta.tree_info.root_index, + }; + + // Build TLV for ATA + let tlv = vec![ExtensionInstructionData::CompressedOnly( + CompressedOnlyExtensionInstructionData { + delegated_amount: 0, + withheld_transfer_fee: 0, + is_frozen: false, + compression_index: 0, + is_ata: true, + bump, + owner_index: packed_ata.wallet_index, + }, + )]; + + decompress_indices.push(DecompressFullIndices { + source, + destination_index: packed_ata.ata_destination_index, + tlv: Some(tlv), + is_ata: true, + }); + } + + Ok(()) +} +``` + +### process_standard_mints (New Function) + +```rust +// sdk-libs/ctoken-sdk/src/compressible/standard_mint.rs (NEW FILE) + +/// Process standard mints via CPI to ctoken program. +#[inline(never)] +pub fn process_standard_mints<'info>( + standard_mints: Vec, + packed_accounts: &[AccountInfo<'info>], + fee_payer: &AccountInfo<'info>, + cpi_accounts: &CpiAccounts<'_, 'info>, + ctoken_config: &AccountInfo<'info>, + ctoken_rent_sponsor: &AccountInfo<'info>, + ctoken_cpi_authority: &AccountInfo<'info>, + proof: ValidityProof, + has_prior_context: bool, + has_subsequent: bool, +) -> Result<(), ProgramError> { + if standard_mints.is_empty() { + return Ok(()); + } + + let mint_count = standard_mints.len(); + let last_idx = mint_count - 1; + + let mints_only = !has_prior_context && !has_subsequent; + let cpi_context_account = if mints_only { + None + } else { + Some(cpi_accounts.cpi_context()?.clone()) + }; + + // Build system accounts once + let system_accounts = SystemAccountInfos { + light_system_program: cpi_accounts.get_account_info(0)?.clone(), + cpi_authority_pda: cpi_accounts.authority()?.clone(), + registered_program_pda: cpi_accounts.registered_program_pda()?.clone(), + account_compression_authority: cpi_accounts.account_compression_authority()?.clone(), + account_compression_program: cpi_accounts.account_compression_program()?.clone(), + system_program: cpi_accounts.system_program()?.clone(), + }; + + let state_tree = cpi_accounts.get_tree_account_info(0)?; + let input_queue = cpi_accounts.get_tree_account_info(1)?; + let output_queue = cpi_accounts.get_tree_account_info(2)?; + + for (idx, packed_mint) in standard_mints.into_iter().enumerate() { + // Get accounts from indices + let mint_seed_info = packed_accounts + .get(packed_mint.mint_seed_index as usize) + .ok_or(ProgramError::NotEnoughAccountKeys)?; + let cmint_info = packed_accounts + .get(packed_mint.cmint_destination_index as usize) + .ok_or(ProgramError::NotEnoughAccountKeys)?; + + // Verify CMint derivation + let (derived_cmint, _) = find_cmint_address(mint_seed_info.key); + if derived_cmint != *cmint_info.key { + msg!( + "StandardMint: derivation mismatch. mint_seed={:?}, expected={:?}, got={:?}", + mint_seed_info.key, derived_cmint, cmint_info.key + ); + return Err(ProgramError::InvalidAccountData); + } + + if mints_only { + // Direct execution + DecompressCMintCpi { + mint_seed: mint_seed_info.clone(), + authority: fee_payer.clone(), // No authority check for decompress + payer: fee_payer.clone(), + cmint: cmint_info.clone(), + compressible_config: ctoken_config.clone(), + rent_sponsor: ctoken_rent_sponsor.clone(), + state_tree: state_tree.clone(), + input_queue: input_queue.clone(), + output_queue: output_queue.clone(), + system_accounts: system_accounts.clone(), + compressed_mint_with_context: packed_mint.compressed_mint_with_context, + proof: ValidityProof(proof.0), + rent_payment: packed_mint.rent_payment, + write_top_up: packed_mint.write_top_up, + }.invoke()?; + } else { + // CPI context batching + let is_first = !has_prior_context && idx == 0; + let is_last = idx == last_idx; + let should_execute = is_last && !has_subsequent; + + let cpi_ctx = if should_execute { + CpiContext { first_set_context: false, set_context: false, ..Default::default() } + } else if is_first { + CpiContext { first_set_context: true, set_context: false, ..Default::default() } + } else { + CpiContext { first_set_context: false, set_context: true, ..Default::default() } + }; + + DecompressCMintCpiWithContext { + mint_seed: mint_seed_info.clone(), + authority: fee_payer.clone(), + payer: fee_payer.clone(), + cmint: cmint_info.clone(), + compressible_config: ctoken_config.clone(), + rent_sponsor: ctoken_rent_sponsor.clone(), + state_tree: state_tree.clone(), + input_queue: input_queue.clone(), + output_queue: output_queue.clone(), + cpi_context_account: cpi_context_account.as_ref().unwrap().clone(), + system_accounts: system_accounts.clone(), + ctoken_cpi_authority: ctoken_cpi_authority.clone(), + compressed_mint_with_context: packed_mint.compressed_mint_with_context, + proof: ValidityProof(proof.0), + rent_payment: packed_mint.rent_payment, + write_top_up: packed_mint.write_top_up, + cpi_context: cpi_ctx, + }.invoke()?; + } + } + + Ok(()) +} +``` + +--- + +## Client-Side Changes + +### decompress_accounts_idempotent (Rewritten) + +```rust +// sdk-libs/compressible-client/src/lib.rs + +/// Build decompress_accounts_idempotent instruction with separate standard types. +#[allow(clippy::too_many_arguments)] +pub fn decompress_accounts_idempotent( + program_id: &Pubkey, + discriminator: &[u8], + // Program-specific accounts + decompressed_pda_addresses: &[Pubkey], + compressed_accounts: &[(CompressedAccount, T)], + // Standard types (NEW) + standard_atas: &[StandardAtaData], + standard_mints: &[StandardMintData], + // Accounts + program_account_metas: &[AccountMeta], + validity_proof_with_context: ValidityProofWithContext, +) -> Result> +where + T: Pack + Clone + std::fmt::Debug, +{ + let mut remaining_accounts = PackedAccounts::default(); + + // Determine if we need CPI context + let has_pdas = !compressed_accounts.is_empty(); + let has_tokens_or_atas = compressed_accounts.iter().any(|(ca, _)| ca.owner == C_TOKEN_PROGRAM_ID.into()) + || !standard_atas.is_empty(); + let has_mints = !standard_mints.is_empty(); + + let needs_cpi_context = (has_pdas as u8 + has_tokens_or_atas as u8 + has_mints as u8) >= 2; + + // Setup system accounts + if needs_cpi_context { + let cpi_context = compressed_accounts.first() + .or_else(|| standard_atas.first().map(|_| /* get from proof */)) + .or_else(|| standard_mints.first().map(|_| /* get from proof */)) + .ok_or("No accounts to process")? + .0.tree_info.cpi_context.unwrap(); + + remaining_accounts.add_system_accounts_v2( + SystemAccountMetaConfig::new_with_cpi_context(*program_id, cpi_context) + )?; + } else { + remaining_accounts.add_system_accounts_v2( + SystemAccountMetaConfig::new(*program_id) + )?; + } + + // Pack output queue + let output_queue = get_output_queue(&validity_proof_with_context.accounts[0].tree_info); + let output_state_tree_index = remaining_accounts.insert_or_get(output_queue); + + // Pack tree infos + let packed_tree_infos = validity_proof_with_context.pack_tree_infos(&mut remaining_accounts); + + // 1. Pack program-specific compressed accounts + let mut typed_compressed_accounts = Vec::new(); + for (i, (compressed_account, data)) in compressed_accounts.iter().enumerate() { + remaining_accounts.insert_or_get(compressed_account.tree_info.queue); + let tree_info = packed_tree_infos.state_trees.as_ref().unwrap().packed_tree_infos[i]; + let packed_data = data.pack(&mut remaining_accounts); + + typed_compressed_accounts.push(CompressedAccountData { + meta: CompressedAccountMetaNoLamportsNoAddress { tree_info, output_state_tree_index }, + data: packed_data, + }); + } + + // 2. Pack standard ATAs + let mut packed_standard_atas = Vec::new(); + for ata in standard_atas { + // Derive ATA address + let (ata_address, _) = derive_ctoken_ata(&ata.wallet, &ata.mint); + + // Insert accounts (wallet as signer) + let wallet_index = remaining_accounts.insert_or_get_config(ata.wallet, true, false); + let mint_index = remaining_accounts.insert_or_get(ata.mint); + let ata_destination_index = remaining_accounts.insert_or_get(ata_address); + + // Pack token data + // CRITICAL: token_data.owner = ATA address (from compressed account) + let owner_index = remaining_accounts.insert_or_get(ata.token_data.owner); // ATA address + let delegate_index = ata.token_data.delegate + .map(|d| remaining_accounts.insert_or_get(d)) + .unwrap_or(0); + + // Get tree info for this account from validity proof + let tree_info_idx = compressed_accounts.len() + packed_standard_atas.len(); + let tree_info = packed_tree_infos.state_trees.as_ref().unwrap().packed_tree_infos[tree_info_idx]; + + packed_standard_atas.push(PackedStandardAtaData { + wallet_index, + mint_index, + ata_destination_index, + token_data: PackedTokenData { + owner_index, + mint_index, + amount: ata.token_data.amount, + has_delegate: ata.token_data.delegate.is_some(), + delegate_index, + version: 3, // ShaFlat + }, + meta: CompressedAccountMetaNoLamportsNoAddress { tree_info, output_state_tree_index }, + }); + } + + // 3. Pack standard mints + let mut packed_standard_mints = Vec::new(); + for mint in standard_mints { + let (cmint_address, _) = find_cmint_address(&mint.mint_seed); + + let mint_seed_index = remaining_accounts.insert_or_get(mint.mint_seed); + let cmint_destination_index = remaining_accounts.insert_or_get(cmint_address); + + let tree_info_idx = compressed_accounts.len() + packed_standard_atas.len() + packed_standard_mints.len(); + let tree_info = packed_tree_infos.state_trees.as_ref().unwrap().packed_tree_infos[tree_info_idx]; + + packed_standard_mints.push(PackedStandardMintData { + mint_seed_index, + cmint_destination_index, + compressed_mint_with_context: mint.compressed_mint_with_context.clone(), + rent_payment: mint.rent_payment, + write_top_up: mint.write_top_up, + meta: CompressedAccountMetaNoLamportsNoAddress { tree_info, output_state_tree_index }, + }); + } + + // Build accounts + let mut accounts = program_account_metas.to_vec(); + let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); + accounts.extend(system_accounts); + + // Add PDA destination accounts + for pda in decompressed_pda_addresses { + accounts.push(AccountMeta::new(*pda, false)); + } + + // Add ATA destination accounts + for ata in standard_atas { + let (ata_address, _) = derive_ctoken_ata(&ata.wallet, &ata.mint); + accounts.push(AccountMeta::new(ata_address, false)); + } + + // Add CMint destination accounts + for mint in standard_mints { + let (cmint_address, _) = find_cmint_address(&mint.mint_seed); + accounts.push(AccountMeta::new(cmint_address, false)); + } + + // Serialize instruction data + let instruction_data = DecompressAccountsIdempotentData { + proof: validity_proof_with_context.proof, + compressed_accounts: typed_compressed_accounts, + standard_atas: packed_standard_atas, + standard_mints: packed_standard_mints, + system_accounts_offset: system_accounts_offset as u8, + }; + + let serialized = instruction_data.try_to_vec()?; + let mut data = Vec::with_capacity(discriminator.len() + serialized.len()); + data.extend_from_slice(discriminator); + data.extend_from_slice(&serialized); + + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) +} +``` + +--- + +## Macro Changes + +### Instruction Handler (Modified) + +```rust +// sdk-libs/macros/src/compressible/instructions.rs + +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, + standard_atas: Vec, // NEW + standard_mints: Vec, // NEW + system_accounts_offset: u8, + #seed_params + ) -> Result<()> { + __processor_functions::process_decompress_accounts_idempotent( + &ctx.accounts, + &ctx.remaining_accounts, + proof, + compressed_accounts, + standard_atas, // NEW + standard_mints, // NEW + system_accounts_offset, + #seed_args + ) + } + }) +} +``` + +### Processor Function (Modified) + +```rust +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, + standard_atas: Vec, + standard_mints: Vec, + system_accounts_offset: u8, + #params + ) -> Result<()> { + light_sdk::compressible::process_decompress_accounts_idempotent( + accounts, + remaining_accounts, + compressed_accounts, + standard_atas, + standard_mints, + proof, + system_accounts_offset, + LIGHT_CPI_SIGNER, + &crate::ID, + #seed_params_arg, + ) + .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) + } + }) +} +``` + +--- + +## Accounts Struct (Same as Option A) + +```rust +#[derive(Accounts)] +pub struct DecompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + pub config: AccountInfo<'info>, + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + + // Required when standard ATAs or Mints present + #[account(mut)] + pub ctoken_rent_sponsor: Option>, + pub ctoken_config: Option>, + pub ctoken_program: Option>, + pub ctoken_cpi_authority: Option>, + + // ... program-specific optional accounts ... +} +``` + +--- + +## Validation Rules + +Same as Option A: + +1. **Standard ATA validation:** + - `wallet` must be signer + - `derive_ctoken_ata(wallet, mint) == ata_destination` + - `token_data.owner == ata_destination` (ATA address) + +2. **Standard Mint validation:** + - `find_cmint_address(mint_seed) == cmint_destination` + - No signature required + +3. **Account requirements:** + - Standard types present => ctoken accounts required + +--- + +## Files to Modify + +1. `sdk-libs/sdk/src/compressible/mod.rs` - Export new types +2. `sdk-libs/sdk/src/compressible/standard_types.rs` (NEW) - PackedStandardAtaData, PackedStandardMintData +3. `sdk-libs/sdk/src/compressible/decompress_runtime.rs` - New signature, delegate to standard handlers +4. `sdk-libs/ctoken-sdk/src/compressible/mod.rs` - Export new functions +5. `sdk-libs/ctoken-sdk/src/compressible/standard_ata.rs` (NEW) - process_standard_atas +6. `sdk-libs/ctoken-sdk/src/compressible/standard_mint.rs` (NEW) - process_standard_mints +7. `sdk-libs/compressible-client/src/lib.rs` - New instruction builder +8. `sdk-libs/compressible-client/src/types.rs` (NEW) - StandardAtaData, StandardMintData +9. `sdk-libs/macros/src/compressible/instructions.rs` - New params in generated code + +--- + +## Migration + +1. All existing callers must update to new instruction format +2. Tests need to pass empty vecs for standard_atas/standard_mints if not using +3. No backward compatibility - clean break + diff --git a/sdk-libs/macros/src/accounts.rs b/sdk-libs/macros/src/accounts.rs deleted file mode 100644 index a4d236e36e..0000000000 --- a/sdk-libs/macros/src/accounts.rs +++ /dev/null @@ -1,641 +0,0 @@ -use proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; -use syn::{ - parse::{Parse, ParseStream}, - parse_quote, - punctuated::Punctuated, - token::PathSep, - Error, Expr, Fields, Ident, ItemStruct, Meta, Path, PathSegment, Result, Stmt, Token, Type, - TypePath, -}; - -pub(crate) fn process_light_system_accounts(input: ItemStruct) -> Result { - let mut output = input.clone(); - - let fields = - match output.fields { - Fields::Named(ref mut fields) => fields, - _ => return Err(Error::new_spanned( - input, - "`light_system_accounts` attribute can only be used with structs that have named fields.", - )), - }; - - let fields_to_add = [ - ("light_system_program", "AccountInfo<'info>"), - ("system_program", "Program<'info, System>"), - ("account_compression_program", "AccountInfo<'info>"), - ]; - let fields_to_add_check = [ - ("registered_program_pda", "AccountInfo<'info>"), - ("noop_program", "AccountInfo<'info>"), - ("account_compression_authority", "AccountInfo<'info>"), - ]; - let existing_field_names: Vec<_> = fields - .named - .iter() - .map(|f| f.ident.as_ref().unwrap().to_string()) - .collect(); - - // TODO: Eventually we want to provide flexibility to override. - // Until then, we error if the fields are manually defined. - for (field_name, field_type) in fields_to_add.iter().chain(fields_to_add_check.iter()) { - if existing_field_names.contains(&field_name.to_string()) { - return Err(syn::Error::new_spanned( - &output, - format!("Field `{}` already exists in the struct.", field_name), - )); - } - - let new_field = syn::Field { - attrs: vec![], - vis: syn::Visibility::Public(syn::token::Pub { - span: proc_macro2::Span::call_site(), - }), - mutability: syn::FieldMutability::None, - ident: Some(syn::Ident::new(field_name, proc_macro2::Span::call_site())), - colon_token: Some(syn::Token![:](proc_macro2::Span::call_site())), - ty: syn::parse_str(field_type)?, - }; - fields.named.push(new_field); - } - - let expanded = quote! { - #output - }; - - Ok(expanded) -} - -struct ParamTypeCheck { - ident: Ident, - ty: Type, -} - -impl ToTokens for ParamTypeCheck { - fn to_tokens(&self, tokens: &mut TokenStream) { - let Self { ident, ty } = self; - let stmt: Stmt = parse_quote! { - let #ident: &#ty = #ident; - }; - stmt.to_tokens(tokens); - } -} - -pub struct InstructionArgs { - param_type_checks: Vec, - param_names: Vec, -} - -impl Parse for InstructionArgs { - fn parse(input: ParseStream) -> Result { - let mut param_type_checks = Vec::new(); - let mut param_names = Vec::new(); - - while !input.is_empty() { - let ident = input.parse::()?; - input.parse::()?; - let ty = input.parse::()?; - - param_names.push(ident.clone()); - param_type_checks.push(ParamTypeCheck { ident, ty }); - - if input.peek(Token![,]) { - input.parse::()?; - } - } - - Ok(InstructionArgs { - param_type_checks, - param_names, - }) - } -} - -/// Takes an input struct annotated with `#[light_accounts]` attribute and -/// then: -/// -/// - Creates a separate struct with `Light` prefix and moves compressed -/// account fields (annotated with `#[light_account]` attribute) to it. As a -/// result, the original struct, later processed by Anchor macros, contains -/// only regular accounts. -/// - Creates an extention trait, with `LightContextExt` prefix, which serves -/// as an extension to `LightContext` and defines these methods: -/// - `check_constraints`, where the checks extracted from `#[light_account]` -/// attributes are performed. -/// - `derive_address_seeds`, where the seeds extracted from -/// `#[light_account]` attributes are used to derive the address. -pub(crate) fn process_light_accounts(input: ItemStruct) -> Result { - let mut anchor_accounts_strct = input.clone(); - - let (_, type_gen, _) = input.generics.split_for_impl(); - - let anchor_accounts_name = input.ident.clone(); - let light_accounts_name = Ident::new(&format!("Light{}", input.ident), Span::call_site()); - let ext_trait_name = Ident::new( - &format!("LightContextExt{}", input.ident), - Span::call_site(), - ); - let params_name = Ident::new(&format!("Params{}", input.ident), Span::call_site()); - - let instruction_params = input - .attrs - .iter() - .find(|attribute| attribute.path().is_ident("instruction")) - .map(|attribute| attribute.parse_args::()) - .transpose()?; - - let mut light_accounts_fields: Punctuated = Punctuated::new(); - - let fields = - match anchor_accounts_strct.fields { - Fields::Named(ref mut fields) => fields, - _ => return Err(Error::new_spanned( - input, - "`light_accounts` attribute can only be used with structs that have named fields.", - )), - }; - - // Fields which should belong to the Anchor instruction struct. - let mut anchor_fields = Punctuated::new(); - // Names of fields which should belong to the Anchor instruction struct. - let mut anchor_field_idents = Vec::new(); - // Names of fields which should belong to the Light instruction struct. - let mut light_field_idents = Vec::new(); - // Names of fields of the Light instruction struct, which should be - // available in constraints. - let mut light_referrable_field_idents = Vec::new(); - let mut constraint_calls = Vec::new(); - let mut derive_address_seed_calls = Vec::new(); - let mut set_address_seed_calls = Vec::new(); - - for field in fields.named.iter() { - let mut light_account = false; - for attr in &field.attrs { - if attr.path().is_ident("light_account") { - light_account = true; - } - } - - if light_account { - light_accounts_fields.push(field.clone()); - light_field_idents.push(field.ident.clone()); - - let field_ident = &field.ident; - - let mut account_args = None; - for attribute in &field.attrs { - let attribute_list = match &attribute.meta { - Meta::List(attribute_list) => attribute_list, - _ => continue, - }; - account_args = Some(syn::parse2::( - attribute_list.tokens.clone(), - )?); - break; - } - let account_args = match account_args { - Some(account_args) => account_args, - None => { - return Err(Error::new_spanned( - input, - "no arguments provided in `light_account`", - )) - } - }; - - if account_args.action != LightAccountAction::Init { - light_referrable_field_idents.push(field.ident.clone()); - } - - if let Some(constraint) = account_args.constraint { - let Constraint { expr, error } = constraint; - let error = match error { - Some(error) => error, - None => parse_quote! { - ::light_sdk::error::LightSdkError::ConstraintViolation - }, - }; - constraint_calls.push(quote! { - if ! ( #expr ) { - return ::anchor_lang::prelude::err!(#error); - } - }); - } - - let seeds = account_args.seeds; - derive_address_seed_calls.push(quote! { - let address_seed = ::light_sdk::address::derive_address_seed( - &#seeds, - &crate::ID, - ); - }); - set_address_seed_calls.push(quote! { - #field_ident.set_address_seed(address_seed); - }) - } else { - anchor_fields.push(field.clone()); - anchor_field_idents.push(field.ident.clone()); - } - } - - fields.named = anchor_fields; - - let light_accounts_strct = if light_accounts_fields.is_empty() { - quote! { - #[derive(::light_sdk::LightAccounts)] - pub struct #light_accounts_name {} - } - } else { - quote! { - #[derive(::light_sdk::LightAccounts)] - pub struct #light_accounts_name { - #light_accounts_fields - } - } - }; - - let light_referrable_fields = if light_referrable_field_idents.is_empty() { - quote! {} - } else { - quote! { - let #light_accounts_name { - #(#light_referrable_field_idents),*, .. - } = &self.light_accounts; - } - }; - let input_fields = match instruction_params { - Some(instruction_params) => { - let param_names = instruction_params.param_names; - let param_type_checks = instruction_params.param_type_checks; - quote! { - let #params_name { #(#param_names),*, .. } = inputs; - #(#param_type_checks)* - } - } - None => quote! {}, - }; - - let expanded = quote! { - #[::light_sdk::light_system_accounts] - #[derive(::anchor_lang::Accounts, ::light_sdk::LightTraits)] - #anchor_accounts_strct - - #light_accounts_strct - - pub trait #ext_trait_name { - fn check_constraints( - &self, - inputs: &#params_name, - ) -> Result<()>; - fn derive_address_seeds( - &mut self, - address_merkle_context: ::light_sdk::tree_info::PackedAddressTreeInfo, - inputs: &#params_name, - ); - } - - impl<'a, 'b, 'c, 'info> #ext_trait_name for ::light_sdk::context::LightContext< - 'a, 'b, 'c, 'info, #anchor_accounts_name #type_gen, #light_accounts_name, - > { - #[allow(unused_parens)] - #[allow(unused_variables)] - fn check_constraints( - &self, - inputs: &#params_name, - ) -> Result<()> { - let #anchor_accounts_name { - #(#anchor_field_idents),*, .. - } = &self.anchor_context.accounts; - #light_referrable_fields - #input_fields - - #(#constraint_calls)* - - Ok(()) - } - - #[allow(unused_variables)] - fn derive_address_seeds( - &mut self, - address_merkle_context: PackedAddressTreeInfo, - inputs: &#params_name, - ) { - let #anchor_accounts_name { - #(#anchor_field_idents),*, .. - } = &self.anchor_context.accounts; - #light_referrable_fields - #input_fields - - let unpacked_address_merkle_context = - ::light_sdk::program_merkle_context::unpack_address_merkle_context( - address_merkle_context, self.anchor_context.remaining_accounts); - - #(#derive_address_seed_calls)* - - let #light_accounts_name { #(#light_field_idents),* } = &mut self.light_accounts; - - #(#set_address_seed_calls)* - } - } - }; - - Ok(expanded) -} - -mod light_account_kw { - // Action - syn::custom_keyword!(init); - syn::custom_keyword!(close); - // Constraint - syn::custom_keyword!(constraint); - // Seeds - syn::custom_keyword!(seeds); -} - -#[derive(Eq, PartialEq)] -pub(crate) enum LightAccountAction { - Init, - Mut, - Close, -} - -pub(crate) struct Constraint { - /// Expression of the constraint, e.g. - /// `my_compressed_acc.owner == signer.key()`. - expr: Expr, - /// Optional error to return. If not specified, the default - /// `LightSdkError::ConstraintViolation` will be used. - error: Option, -} - -pub(crate) struct LightAccountArgs { - action: LightAccountAction, - constraint: Option, - seeds: Option, -} - -impl Parse for LightAccountArgs { - fn parse(input: ParseStream) -> Result { - let mut action = None; - let mut constraint = None; - let mut seeds = None; - - while !input.is_empty() { - let lookahead = input.lookahead1(); - - // Actions - if lookahead.peek(light_account_kw::init) { - input.parse::()?; - action = Some(LightAccountAction::Init); - } else if lookahead.peek(Token![mut]) { - input.parse::()?; - action = Some(LightAccountAction::Mut); - } else if lookahead.peek(light_account_kw::close) { - input.parse::()?; - action = Some(LightAccountAction::Close); - } - // Constraint - else if lookahead.peek(light_account_kw::constraint) { - // Parse the constraint. - input.parse::()?; - input.parse::()?; - let expr: Expr = input.parse()?; - - // Parse an optional error. - let mut error = None; - if input.peek(Token![@]) { - input.parse::()?; - error = Some(input.parse::()?); - } - - constraint = Some(Constraint { expr, error }); - } - // Seeds - else if lookahead.peek(light_account_kw::seeds) { - input.parse::()?; - input.parse::()?; - seeds = Some(input.parse::()?); - } else { - return Err(lookahead.error()); - } - - if input.peek(Token![,]) { - input.parse::()?; - } - } - - let action = match action { - Some(action) => action, - None => { - return Err(Error::new( - Span::call_site(), - "Expected an action for the account (`init`, `mut` or `close`)", - )) - } - }; - - Ok(Self { - action, - constraint, - seeds, - }) - } -} - -pub(crate) fn process_light_accounts_derive(input: ItemStruct) -> Result { - let strct_name = &input.ident; - let (impl_gen, type_gen, where_clause) = input.generics.split_for_impl(); - - let mut try_from_slice_calls = Vec::new(); - let mut field_idents = Vec::new(); - let mut new_address_params_calls = Vec::new(); - let mut input_account_calls = Vec::new(); - let mut output_account_calls = Vec::new(); - - let fields = match input.fields { - Fields::Named(ref fields) => fields, - _ => { - return Err(Error::new_spanned( - input, - "Only structs with named fields can derive LightAccounts", - )) - } - }; - - for (i, field) in fields.named.iter().enumerate() { - let field_ident = &field.ident; - field_idents.push(field_ident); - - let account_args = field - .attrs - .iter() - .find(|attribute| attribute.path().is_ident("light_account")) - .map(|attribute| attribute.parse_args::()) - .transpose()? - .ok_or_else(|| { - Error::new_spanned(input.clone(), "no arguments provided in `light_account`") - })?; - - let type_path = match field.ty { - Type::Path(ref type_path) => type_path, - _ => { - return Err(Error::new_spanned( - input, - "Only struct with typed fields can derive LightAccounts", - )) - } - }; - - let type_path_without_args = TypePath { - qself: type_path.qself.clone(), - path: Path { - leading_colon: type_path.path.leading_colon, - segments: type_path - .path - .segments - .iter() - .map(|segment| PathSegment { - ident: segment.ident.clone(), - arguments: syn::PathArguments::None, - }) - .collect::>(), - }, - }; - let try_from_slice_call = match account_args.action { - LightAccountAction::Init => quote! { - let mut #field_ident: #type_path = #type_path_without_args::new_init( - &merkle_context, - &address_merkle_context, - address_merkle_tree_root_index, - ); - }, - LightAccountAction::Mut => quote! { - let mut #field_ident: #type_path = #type_path_without_args::try_from_slice_mut( - inputs[#i].as_slice(), - &merkle_context, - merkle_tree_root_index, - &address_merkle_context, - )?; - }, - LightAccountAction::Close => quote! { - let mut #field_ident: #type_path = #type_path_without_args::try_from_slice_close( - inputs[#i].as_slice(), - &merkle_context, - merkle_tree_root_index, - &address_merkle_context, - )?; - }, - }; - try_from_slice_calls.push(try_from_slice_call); - - new_address_params_calls.push(quote! { - if let Some(new_address_params_for_acc) = self.#field_ident.new_address_params() { - new_address_params.push(new_address_params_for_acc); - } - }); - input_account_calls.push(quote! { - if let Some(compressed_account) = self.#field_ident.input_compressed_account( - &crate::ID, - remaining_accounts, - )? { - accounts.push(compressed_account); - } - }); - output_account_calls.push(quote! { - if let Some(compressed_account) = self.#field_ident.output_compressed_account( - &crate::ID, - remaining_accounts, - )? { - accounts.push(compressed_account); - } - }); - } - - let expanded = quote! { - impl #impl_gen ::light_sdk::compressed_account::LightAccounts for #strct_name #type_gen #where_clause { - fn try_light_accounts( - inputs: Vec>, - merkle_context: ::light_sdk::merkle_context::PackedMerkleContext, - merkle_tree_root_index: u16, - address_merkle_context: ::light_sdk::tree_info::PackedAddressTreeInfo, - address_merkle_tree_root_index: u16, - remaining_accounts: &[::anchor_lang::prelude::AccountInfo], - ) -> Result { - let unpacked_address_merkle_context = - ::light_sdk::program_merkle_context::unpack_address_merkle_context( - address_merkle_context, remaining_accounts); - - #(#try_from_slice_calls)* - Ok(Self { - #(#field_idents),* - }) - } - - fn new_address_params(&self) -> Vec<::light_sdk::address::NewAddressParamsPacked> { - let mut new_address_params = Vec::new(); - #(#new_address_params_calls)* - new_address_params - } - - fn input_accounts(&self, remaining_accounts: &[::anchor_lang::prelude::AccountInfo]) -> Result> { - let mut accounts = Vec::new(); - #(#input_account_calls)* - Ok(accounts) - } - - fn output_accounts(&self, remaining_accounts: &[::anchor_lang::prelude::AccountInfo]) -> Result> { - let mut accounts = Vec::new(); - #(#output_account_calls)* - Ok(accounts) - } - } - }; - - Ok(expanded) -} - -#[cfg(test)] -mod tests { - use syn::{parse_quote, ItemStruct}; - - use super::*; - - #[test] - fn test_process_light_system_accounts_adds_fields_correctly() { - let input: ItemStruct = parse_quote! { - struct TestStruct { - #[light_account(mut)] - foo: u64, - existing_field: u32, - } - }; - - let output = process_light_system_accounts(input).unwrap(); - let output_string = output.to_string(); - - println!("{output_string}"); - - assert!(output_string.contains("light_system_program")); - assert!(output_string.contains("system_program")); - assert!(output_string.contains("account_compression_program")); - assert!(output_string.contains("registered_program_pda")); - assert!(output_string.contains("noop_program")); - assert!(output_string.contains("account_compression_authority")); - } - - #[test] - fn test_process_light_system_accounts_fails_on_existing_field() { - let input: ItemStruct = parse_quote! { - struct TestStruct { - existing_field: u32, - system_program: Program<'info, System>, - } - }; - - let result = process_light_system_accounts(input); - assert!(result.is_err()); - let error_message = result.unwrap_err().to_string(); - assert!(error_message.contains("Field `system_program` already exists in the struct.")); - } -} diff --git a/sdk-libs/macros/src/compressible/GUIDE.md b/sdk-libs/macros/src/compressible/GUIDE.md deleted file mode 100644 index 599db88b56..0000000000 --- a/sdk-libs/macros/src/compressible/GUIDE.md +++ /dev/null @@ -1,198 +0,0 @@ -## Compressible macros — caller program usage (first draft) - -Use this to add rent-free PDAs, cTokens, and cMints to your program with minimal boilerplate. - -### What you get (the interface) - -- `#[derive(Compressible)]`: makes a struct compressible. Expect a `compression_info: Option` field. -- `#[add_compressible_instructions(...)]`: generates ready-to-use `decompress_accounts_idempotent` and `compress_accounts_idempotent` entrypoints, PDA seed derivation, and optional cToken integration. -- `#[account]`: convenience macro for Anchor accounts adding `LightHasher` + `LightDiscriminator` derives. -- Rent tools: `derive_light_rent_sponsor_pda!`, `derive_light_rent_sponsor!` for compile‑time rent sponsor constants. -- Program config helpers: `process_initialize_compression_config_checked`, `process_update_compression_config`. - -### How to use — PDA only - -1. Define your PDAs - -```rust -use light_sdk::compressible::CompressionInfo; -use light_sdk_macros::{account, Compressible}; - -#[account] -#[derive(Compressible)] -pub struct UserRecord { - pub compression_info: Option, - pub owner: Pubkey, - pub name: String, - pub score: u64, -} -``` - -2. Generate compress/decompress instructions with auto seeds - -```rust -use light_sdk_macros::add_compressible_instructions; - -#[add_compressible_instructions( - UserRecord = ("user_record", data.owner.as_ref()) -)] -#[program] -pub mod my_program {} -``` - -3. Initialize your compression config (one-time) - -- Call the generated `initialize_compression_config` entrypoint or invoke: - - `process_initialize_compression_config_checked(config_pda, update_authority, program_data, rent_sponsor, compression_authority, rent_config, write_top_up, address_space, bump=0, payer, system_program, program_id)` -- Inputs you must pick: - - rent_sponsor: who receives rent when PDAs compress/close - - compression_authority: who can compress/close your PDAs - - rent_config + write_top_up: rent curve + write top‑up per write - - address_space: one address tree pubkey for your PDAs - -4. Use the generated entrypoints - -- `decompress_accounts_idempotent(...)` -- `compress_accounts_idempotent(...)` - -### How to use — mixed with cToken - -1. Extend the macro with token variants - -```rust -#[add_compressible_instructions( - // PDAs - UserRecord = ("user_record", data.owner.as_ref()), - // Program‑owned ctoken PDA (must provide authority seeds) - TreasuryCtoken = (is_token, "treasury_ctoken", ctx.fee_payer, authority = (ctx.treasury)), - // User ATA variant (no seeds, derived from owner+mint) - UserAta = (is_token, is_ata) -)] -#[program] -pub mod my_program {} -``` - -2. Create compressible token accounts (ATAs) on the client or via CPI - -- Inputs (client builder): `CreateCompressibleAssociatedTokenAccountInputs { payer, owner, mint, compressible_config, rent_sponsor, pre_pay_num_epochs, lamports_per_write, token_account_version }` -- Authority-less user ATAs use `derive_ctoken_ata(owner, mint)` under the hood. - -3. Decompress/compress flows - -- The generated `decompress_accounts_idempotent` and `compress_accounts_idempotent` accept packed token data alongside your PDAs. You only provide the standard accounts the macro adds (fee_payer, config, rent_sponsor, and optional ctoken config/cpi auth). - -### How to use — cMints (compressed mints) - -- Create a compressed mint: - - `create_compressed_mint(CreateCompressedMintInputs { decimals, mint_authority, freeze_authority, proof, address_merkle_tree_root_index, mint_signer, payer, address_tree_pubkey, output_queue, extensions, version })` - - Derive addresses with: - - `derive_mint_compressed_address(&mint_signer, &address_tree_pubkey)` - - `find_mint_address(&mint_signer)` -- Mint tokens to compressed accounts: - - `create_mint_to_compressed_instruction(MintToCompressedInputs { compressed_mint_inputs, recipients, mint_authority, payer, state_merkle_tree, input_queue, output_queue_cmint, output_queue_tokens, decompressed_mint_config, proof, token_account_version, cpi_context_pubkey, token_pool })` - -Keep it simple: create cMint → mint to recipients (compressed accounts or cToken ATAs) using the SDK helpers below. - -### cToken SDK (compressed-token-sdk) — the interfaces you actually call - -- Accounts - - `derive_ctoken_ata(owner, mint) -> (Pubkey, u8)` - - `create_compressible_associated_token_account(inputs)` / `_idempotent` (+ “2” variants if owner/mint passed as accounts) - - Low-level: `create_compressible_token_account_instruction(CreateCompressibleTokenAccount)` -- Mints - - `create_compressed_mint(CreateCompressedMintInputs)` - - `derive_mint_compressed_address(mint_seed, address_tree)` - - `find_mint_address(mint_seed)` -- Mint to recipients - - `create_mint_to_compressed_instruction(MintToCompressedInputs)` - - Types: `Recipient { recipient, amount }` -- Transfer SPL ↔ cToken - - `create_transfer_spl_to_ctoken_instruction(...)` - - `create_transfer_ctoken_to_spl_instruction(...)` - - `transfer_interface(...)` / `transfer_interface_signed(...)` -- Update compressed mint - - `update_compressed_mint(UpdateCompressedMintInputs)` - -### Rent — set/update for your PDAs and for cTokens - -- PDAs (your program) - - One-time config: `process_initialize_compression_config_checked(...)` (or use generated `initialize_compression_config` entrypoint) - - Update later: `process_update_compression_config(config, authority, new_update_authority?, new_rent_sponsor?, new_compression_authority?, new_rent_config?, new_write_top_up?, new_address_space?, program_id)` - - Use `light_compressible::rent::RentConfig` to define rent curve and distribution. Funds on close/compress go to `rent_sponsor` (completed epochs) and refund fee payer for partial epochs automatically. -- cTokens (account-level) - - When creating a compressible token account, you pass: - - `rent_sponsor`, `pre_pay_num_epochs`, optional `lamports_per_write`, and `compressible_config` (the registry’s or your chosen config PDA) - - For ATAs: `CreateCompressibleAssociatedTokenAccountInputs { ... }` - -### Rust client — the minimum you need - -1. Connect and fetch proofs - -```rust -use light_client::rpc::{LightClient, LightClientConfig, Rpc}; - -let mut rpc = LightClient::new(LightClientConfig::local()).await?; // or devnet/mainnet -// rpc.get_validity_proof(account_hashes, new_addresses, None).await? -``` - -2. Create a compressible ATA - -```rust -use light_token_sdk::instructions::{ - create_compressible_associated_token_account, CreateCompressibleAssociatedTokenAccountInputs -}; - -let ix = create_compressible_associated_token_account(CreateCompressibleAssociatedTokenAccountInputs { - payer, - owner, - mint, - compressible_config, - rent_sponsor, - pre_pay_num_epochs: 2, - lamports_per_write: Some(1_000), - token_account_version: light_token_interface::state::TokenDataVersion::ShaFlat, -})?; -``` - -3. Create a cMint and mint to recipients - -```rust -use light_token_sdk::instructions::{ - create_compressed_mint, CreateCompressedMintInputs, - create_mint_to_compressed_instruction, MintToCompressedInputs -}; -use light_token_interface::instructions::mint_action::Recipient; - -let create_cmint_ix = create_compressed_mint(CreateCompressedMintInputs { /* fill from RPC + keys */ })?; -let mint_ix = create_mint_to_compressed_instruction(MintToCompressedInputs { - recipients: vec![Recipient { recipient: some_address, amount: 1000 }], - /* queues/tree/authority from RPC + keys */ -}, None)?; -``` - -4. High-level helpers (token-client) - -```rust -use light_token_client::actions::{create_compressible_token_account, CreateCompressibleTokenAccountInputs, mint_to_compressed}; - -let token_acc = create_compressible_token_account(&mut rpc, CreateCompressibleTokenAccountInputs { - owner, mint, num_prepaid_epochs: 2, payer: &payer, token_account_keypair: None, - lamports_per_write: None, token_account_version: light_token_interface::state::TokenDataVersion::ShaFlat -}).await?; - -let sig = mint_to_compressed(&mut rpc, spl_mint_pda, vec![Recipient{ recipient: token_acc, amount: 1000 }], light_token_interface::state::TokenDataVersion::ShaFlat, &mint_authority, &payer).await?; -``` - -### TL;DR checklists - -- PDA only - - Add `#[derive(Compressible)]` + `compression_info` - - Add `#[add_compressible_instructions(...)]` with seeds - - Initialize config (rent_sponsor, compression_authority, rent_config, write_top_up, address_space) - - Call generated compress/decompress entrypoints -- Mixed with cToken - - Add token variants in `#[add_compressible_instructions(...)]` (program-owned with `authority = (...)` or `is_ata`) - - Use SDK to create cToken ATAs; pass rent fields - - Mint via cMints and `mint_to_compressed` or `mint_action` -- cMints - - `create_compressed_mint(...)` then `create_mint_to_compressed_instruction(...)` diff --git a/sdk-libs/macros/src/compressible/README.md b/sdk-libs/macros/src/compressible/README.md index eb7337d103..7957f520d8 100644 --- a/sdk-libs/macros/src/compressible/README.md +++ b/sdk-libs/macros/src/compressible/README.md @@ -19,13 +19,13 @@ Procedural macros for generating rent-free account types and their hooks for Sol **`variant_enum.rs`** - Account variant enum -- Generates `CompressedAccountVariant` enum from account types +- Generates `RentFreeAccountVariant` enum from account types - Implements all required traits (Default, DataHasher, Size, Pack, Unpack) -- Creates `CompressedAccountData` wrapper struct +- Creates `RentFreeAccountData` wrapper struct **`instructions.rs`** - Instruction generation -- Main macro: `add_compressible_instructions` +- Main macro: `#[compressible]` - Generates compress/decompress instruction handlers - Creates context structs and account validation - **Compress**: PDA-only (ctokens compressed via registry) diff --git a/sdk-libs/macros/src/compressible/anchor_seeds.rs b/sdk-libs/macros/src/compressible/anchor_seeds.rs new file mode 100644 index 0000000000..cfee10eb00 --- /dev/null +++ b/sdk-libs/macros/src/compressible/anchor_seeds.rs @@ -0,0 +1,677 @@ +//! Anchor seed extraction from #[account(seeds = [...])] attributes. +//! +//! This module extracts PDA seeds from Anchor's attribute syntax and classifies them +//! into the categories needed for compression: literals, ctx fields, data fields, etc. + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{parse::Parse, Expr, Ident, ItemStruct, Type}; + +/// Classified seed element from Anchor's seeds array +#[derive(Clone, Debug)] +pub enum ClassifiedSeed { + /// b"literal" or "string" - hardcoded bytes + Literal(Vec), + /// CONSTANT - uppercase identifier, resolved as crate::CONSTANT + Constant(syn::Path), + /// account.key().as_ref() - reference to account in struct + CtxAccount(Ident), + /// params.field.as_ref() or params.field.to_le_bytes().as_ref() + DataField { + field_name: Ident, + /// Method like to_le_bytes, or None for direct .as_ref() + conversion: Option, + }, + /// Function call like max_key(&a.key(), &b.key()) + FunctionCall { + func: syn::Path, + /// Account references used as arguments + ctx_args: Vec, + }, +} + +/// Extracted seed specification for a compressible field +#[derive(Clone, Debug)] +pub struct ExtractedSeedSpec { + /// The field name in the Accounts struct + pub field_name: Ident, + /// The inner type (e.g., UserRecord from Account<'info, UserRecord>) + pub inner_type: Ident, + /// Whether it's Box> + pub is_boxed: bool, + /// Classified seeds from #[account(seeds = [...])] + pub seeds: Vec, +} + +/// Extracted token specification for a #[rentfree_token = Variant] field +#[derive(Clone, Debug)] +pub struct ExtractedTokenSpec { + /// The field name in the Accounts struct + pub field_name: Ident, + /// The variant name from #[rentfree_token = Variant] + pub variant_name: Ident, + /// Seeds from #[account(seeds = [...])] + pub seeds: Vec, + /// Authority field name (if specified or auto-detected) + pub authority_field: Option, + /// Authority seeds (from the authority field's #[account(seeds)]) + pub authority_seeds: Option>, +} + +/// All extracted info from an Accounts struct +#[derive(Clone, Debug)] +pub struct ExtractedAccountsInfo { + pub struct_name: Ident, + pub pda_fields: Vec, + pub token_fields: Vec, + /// All fields in the struct (for authority lookup) + pub all_fields: Vec<(Ident, Type)>, +} + +/// Extract rentfree field info from an Accounts struct +pub fn extract_from_accounts_struct( + item: &ItemStruct, +) -> syn::Result> { + let fields = match &item.fields { + syn::Fields::Named(named) => &named.named, + _ => return Ok(None), + }; + + let mut pda_fields = Vec::new(); + let mut token_fields = Vec::new(); + let mut all_fields = Vec::new(); + + for field in fields { + let field_ident = match &field.ident { + Some(id) => id.clone(), + None => continue, + }; + + all_fields.push((field_ident.clone(), field.ty.clone())); + + // Check for #[rentfree] attribute + let has_rentfree = field + .attrs + .iter() + .any(|attr| attr.path().is_ident("rentfree")); + + // Check for #[rentfree_token(...)] attribute + let token_attr = extract_rentfree_token_attr(&field.attrs); + + if has_rentfree { + // Extract inner type from Account<'info, T> or Box> + let (is_boxed, inner_type) = match extract_account_inner_type(&field.ty) { + Some(result) => result, + None => { + return Err(syn::Error::new_spanned( + &field.ty, + "#[rentfree] requires Account<'info, T> or Box>", + )); + } + }; + + // Extract seeds from #[account(seeds = [...])] + let seeds = extract_anchor_seeds(&field.attrs)?; + + pda_fields.push(ExtractedSeedSpec { + field_name: field_ident, + inner_type, + is_boxed, + seeds, + }); + } else if let Some(token_attr) = token_attr { + // Token field with explicit variant mapping + let seeds = extract_anchor_seeds(&field.attrs)?; + + token_fields.push(ExtractedTokenSpec { + field_name: field_ident, + variant_name: token_attr.variant_name, + seeds, + authority_field: None, + // Use authority from attribute if provided + authority_seeds: token_attr.authority_seeds, + }); + } + } + + // If no rentfree fields found, return None + if pda_fields.is_empty() && token_fields.is_empty() { + return Ok(None); + } + + // Resolve authority for token fields (only if not already provided in attribute) + for token in &mut token_fields { + // Skip if authority was already provided in the attribute + if token.authority_seeds.is_some() { + continue; + } + + // Try to find authority field by convention: {field_name}_authority or vault_authority + let authority_candidates = [ + format!("{}_authority", token.field_name), + "vault_authority".to_string(), + "authority".to_string(), + ]; + + for candidate in &authority_candidates { + if let Some((auth_field, _)) = all_fields.iter().find(|(name, _)| name == candidate) { + token.authority_field = Some(auth_field.clone()); + + // Try to extract authority seeds from the authority field + if let Some(auth_field_info) = fields + .iter() + .find(|f| f.ident.as_ref().map(|i| i.to_string()) == Some(candidate.clone())) + { + if let Ok(auth_seeds) = extract_anchor_seeds(&auth_field_info.attrs) { + if !auth_seeds.is_empty() { + token.authority_seeds = Some(auth_seeds); + } + } + } + break; + } + } + } + + Ok(Some(ExtractedAccountsInfo { + struct_name: item.ident.clone(), + pda_fields, + token_fields, + all_fields, + })) +} + +/// Parsed #[rentfree_token(...)] attribute +struct RentFreeTokenAttr { + variant_name: Ident, + authority_seeds: Option>, +} + +/// Extract #[rentfree_token(Variant, authority = [...])] attribute +fn extract_rentfree_token_attr(attrs: &[syn::Attribute]) -> Option { + for attr in attrs { + if attr.path().is_ident("rentfree_token") { + match &attr.meta { + // #[rentfree_token = Variant] + syn::Meta::NameValue(nv) => { + if let Expr::Path(path) = &nv.value { + if let Some(ident) = path.path.get_ident() { + return Some(RentFreeTokenAttr { + variant_name: ident.clone(), + authority_seeds: None, + }); + } + } + } + // #[rentfree_token(Variant)] or #[rentfree_token(Variant, authority = [...])] + syn::Meta::List(list) => { + if let Ok(parsed) = parse_rentfree_token_list(&list.tokens) { + return Some(parsed); + } + // Fallback: try parsing as just an identifier + if let Ok(ident) = syn::parse2::(list.tokens.clone()) { + return Some(RentFreeTokenAttr { + variant_name: ident, + authority_seeds: None, + }); + } + } + _ => {} + } + } + } + None +} + +/// Parse rentfree_token(Variant, authority = [...]) content +fn parse_rentfree_token_list( + tokens: &proc_macro2::TokenStream, +) -> syn::Result { + use syn::parse::Parser; + + let parser = |input: syn::parse::ParseStream| -> syn::Result { + // First token is the variant name + let variant_name: Ident = input.parse()?; + let mut authority_seeds = None; + + // Check for comma and additional args + while input.peek(syn::Token![,]) { + input.parse::()?; + + // Look for authority = [...] + if input.peek(Ident) { + let key: Ident = input.parse()?; + if key == "authority" { + input.parse::()?; + let array: syn::ExprArray = input.parse()?; + // Classify the authority seeds + let mut seeds = Vec::new(); + for elem in &array.elems { + if let Ok(seed) = classify_seed_expr(elem) { + seeds.push(seed); + } + } + authority_seeds = Some(seeds); + } + } + } + + Ok(RentFreeTokenAttr { + variant_name, + authority_seeds, + }) + }; + + parser.parse2(tokens.clone()) +} + +/// Extract inner type T from Account<'info, T>, Box>, +/// AccountLoader<'info, T>, or InterfaceAccount<'info, T> +fn extract_account_inner_type(ty: &Type) -> Option<(bool, Ident)> { + match ty { + Type::Path(type_path) => { + let segment = type_path.path.segments.last()?; + let ident_str = segment.ident.to_string(); + + match ident_str.as_str() { + "Account" | "AccountLoader" | "InterfaceAccount" => { + // Extract T from Account<'info, T> + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + for arg in &args.args { + if let syn::GenericArgument::Type(Type::Path(inner_path)) = arg { + if let Some(inner_seg) = inner_path.path.segments.last() { + // Skip lifetime 'info + if inner_seg.ident != "info" { + return Some((false, inner_seg.ident.clone())); + } + } + } + } + } + None + } + "Box" => { + // Check for Box> + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + if let Some((_, inner_type)) = extract_account_inner_type(inner_ty) { + return Some((true, inner_type)); + } + } + } + None + } + _ => None, + } + } + _ => None, + } +} + +/// Extract seeds from #[account(seeds = [...], bump)] attribute +fn extract_anchor_seeds(attrs: &[syn::Attribute]) -> syn::Result> { + for attr in attrs { + if !attr.path().is_ident("account") { + continue; + } + + // Parse the attribute as a token stream and look for seeds = [...] + let tokens = match &attr.meta { + syn::Meta::List(list) => list.tokens.clone(), + _ => continue, + }; + + // Parse as comma-separated key-value pairs + let parsed: syn::Result> = + syn::parse::Parser::parse2( + syn::punctuated::Punctuated::parse_terminated, + tokens.clone(), + ); + + if let Ok(items) = &parsed { + for item in items { + if item.key == "seeds" { + return classify_seeds_array(&item.value); + } + } + } + } + + Ok(Vec::new()) +} + +/// Helper struct for parsing account attribute items +struct AccountAttrItem { + key: Ident, + value: Expr, +} + +impl syn::parse::Parse for AccountAttrItem { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + // Handle keywords like `mut` as well as identifiers + let key: Ident = if input.peek(syn::Token![mut]) { + input.parse::()?; + Ident::new("mut", proc_macro2::Span::call_site()) + } else { + input.parse()? + }; + + // Handle bare identifiers like `mut`, `init`, `bump` + if !input.peek(syn::Token![=]) { + return Ok(AccountAttrItem { + key: key.clone(), + value: syn::parse_quote!(true), + }); + } + + input.parse::()?; + let value: Expr = input.parse()?; + + Ok(AccountAttrItem { key, value }) + } +} + +/// Classify seeds from an array expression [seed1, seed2, ...] +fn classify_seeds_array(expr: &Expr) -> syn::Result> { + let array = match expr { + Expr::Array(arr) => arr, + Expr::Reference(r) => { + if let Expr::Array(arr) = &*r.expr { + arr + } else { + return Err(syn::Error::new_spanned(expr, "Expected seeds array")); + } + } + _ => return Err(syn::Error::new_spanned(expr, "Expected seeds array")), + }; + + let mut seeds = Vec::new(); + for elem in &array.elems { + seeds.push(classify_seed_expr(elem)?); + } + + Ok(seeds) +} + +/// Classify a single seed expression +fn classify_seed_expr(expr: &Expr) -> syn::Result { + match expr { + // b"literal" + Expr::Lit(lit) => { + if let syn::Lit::ByteStr(bs) = &lit.lit { + return Ok(ClassifiedSeed::Literal(bs.value())); + } + if let syn::Lit::Str(s) = &lit.lit { + return Ok(ClassifiedSeed::Literal(s.value().into_bytes())); + } + Err(syn::Error::new_spanned( + expr, + "Unsupported literal in seeds", + )) + } + + // CONSTANT (all uppercase path) + Expr::Path(path) => { + if let Some(ident) = path.path.get_ident() { + let name = ident.to_string(); + if name + .chars() + .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) + { + return Ok(ClassifiedSeed::Constant(path.path.clone())); + } + // Otherwise it's a variable reference - treat as ctx account + return Ok(ClassifiedSeed::CtxAccount(ident.clone())); + } + // Multi-segment path is a constant + Ok(ClassifiedSeed::Constant(path.path.clone())) + } + + // method_call.as_ref() - most common case + Expr::MethodCall(mc) => classify_method_call(mc), + + // Reference like &account.key() + Expr::Reference(r) => classify_seed_expr(&r.expr), + + // Field access like params.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, + }); + } + } + } + // ctx.field or account.field - treat as ctx account + return Ok(ClassifiedSeed::CtxAccount(field_name.clone())); + } + Err(syn::Error::new_spanned( + expr, + "Unsupported field expression", + )) + } + + // Function call like max_key(&a.key(), &b.key()).as_ref() + Expr::Call(call) => { + let func = match &*call.func { + Expr::Path(p) => p.path.clone(), + _ => { + return Err(syn::Error::new_spanned( + expr, + "Expected path for function call", + )) + } + }; + + let mut ctx_args = Vec::new(); + for arg in &call.args { + if let Some(ident) = extract_ctx_ident_from_expr(arg) { + ctx_args.push(ident); + } + } + + Ok(ClassifiedSeed::FunctionCall { func, ctx_args }) + } + + _ => Err(syn::Error::new_spanned( + expr, + format!("Unsupported seed expression: {:?}", expr), + )), + } +} + +/// Classify a method call expression like account.key().as_ref() +fn classify_method_call(mc: &syn::ExprMethodCall) -> syn::Result { + // Unwrap .as_ref() at the end + if mc.method == "as_ref" { + 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 account.key() + if mc.method == "key" { + if let Some(ident) = extract_receiver_ident(&mc.receiver) { + // 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, + }); + } + } + } + } + } + return Ok(ClassifiedSeed::CtxAccount(ident)); + } + } + + // params.field.as_ref() directly + if let Some((field_name, base)) = extract_params_field(&mc.receiver) { + if base == "params" { + return Ok(ClassifiedSeed::DataField { + field_name, + conversion: None, + }); + } + } + + Err(syn::Error::new_spanned( + mc, + "Unsupported method call in seeds", + )) +} + +/// 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())); + } + } + } + } + None +} + +/// Extract the base identifier from an expression like account.key() -> account +fn extract_receiver_ident(expr: &Expr) -> Option { + match expr { + Expr::Path(path) => path.path.get_ident().cloned(), + Expr::Field(field) => { + if let syn::Member::Named(name) = &field.member { + Some(name.clone()) + } else { + None + } + } + Expr::MethodCall(mc) => extract_receiver_ident(&mc.receiver), + Expr::Reference(r) => extract_receiver_ident(&r.expr), + _ => None, + } +} + +/// Extract ctx account identifier from expression (for function args) +fn extract_ctx_ident_from_expr(expr: &Expr) -> Option { + match expr { + Expr::Reference(r) => extract_ctx_ident_from_expr(&r.expr), + Expr::MethodCall(mc) => { + if mc.method == "key" { + extract_receiver_ident(&mc.receiver) + } else { + None + } + } + Expr::Field(field) => { + if let syn::Member::Named(name) = &field.member { + Some(name.clone()) + } else { + None + } + } + Expr::Path(path) => path.path.get_ident().cloned(), + _ => None, + } +} + +/// Generate seed derivation code from classified seeds +pub fn generate_seed_derivation(seeds: &[ClassifiedSeed]) -> TokenStream { + let seed_exprs: Vec = seeds + .iter() + .map(|seed| match seed { + ClassifiedSeed::Literal(bytes) => { + quote! { &[#(#bytes),*] } + } + ClassifiedSeed::Constant(path) => { + quote! { crate::#path.as_ref() } + } + ClassifiedSeed::CtxAccount(ident) => { + quote! { ctx_seeds.#ident.as_ref() } + } + ClassifiedSeed::DataField { + field_name, + conversion: None, + } => { + quote! { self.#field_name.as_ref() } + } + ClassifiedSeed::DataField { + field_name, + conversion: Some(method), + } => { + quote! { self.#field_name.#method().as_ref() } + } + ClassifiedSeed::FunctionCall { func, ctx_args } => { + let args: Vec = ctx_args + .iter() + .map(|arg| quote! { &ctx_seeds.#arg }) + .collect(); + quote! { #func(#(#args),*).as_ref() } + } + }) + .collect(); + + quote! { + let seeds: &[&[u8]] = &[#(#seed_exprs),*]; + } +} + +/// Get ctx field names from classified seeds +pub fn get_ctx_fields(seeds: &[ClassifiedSeed]) -> Vec { + let mut fields = Vec::new(); + for seed in seeds { + match seed { + ClassifiedSeed::CtxAccount(ident) => { + if !fields.iter().any(|f: &Ident| f == ident) { + fields.push(ident.clone()); + } + } + ClassifiedSeed::FunctionCall { ctx_args, .. } => { + for arg in ctx_args { + if !fields.iter().any(|f: &Ident| f == arg) { + fields.push(arg.clone()); + } + } + } + _ => {} + } + } + fields +} + +/// Get data field names from classified seeds +pub fn get_data_fields(seeds: &[ClassifiedSeed]) -> Vec<(Ident, Option)> { + let mut fields = Vec::new(); + for seed in seeds { + if let ClassifiedSeed::DataField { + field_name, + conversion, + } = seed + { + if !fields.iter().any(|(f, _): &(Ident, _)| f == field_name) { + fields.push((field_name.clone(), conversion.clone())); + } + } + } + fields +} diff --git a/sdk-libs/macros/src/compressible/decompress_context.rs b/sdk-libs/macros/src/compressible/decompress_context.rs index 5d5a37301e..12cc0c440e 100644 --- a/sdk-libs/macros/src/compressible/decompress_context.rs +++ b/sdk-libs/macros/src/compressible/decompress_context.rs @@ -2,78 +2,122 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - DeriveInput, Ident, Result, Token, -}; - -struct PdaTypesAttr { - types: Punctuated, -} +use syn::{Ident, Result}; -impl Parse for PdaTypesAttr { - fn parse(input: ParseStream) -> Result { - Ok(PdaTypesAttr { - types: Punctuated::parse_terminated(input)?, - }) - } -} - -struct TokenVariantAttr { - variant: Ident, -} - -impl Parse for TokenVariantAttr { - fn parse(input: ParseStream) -> Result { - Ok(TokenVariantAttr { - variant: input.parse()?, - }) - } -} +// Re-export from variant_enum for convenience +pub use crate::compressible::variant_enum::PdaCtxSeedInfo; pub fn generate_decompress_context_trait_impl( - pda_type_idents: Vec, + pda_ctx_seeds: Vec, token_variant_ident: Ident, lifetime: syn::Lifetime, ) -> Result { - let pda_match_arms: Vec<_> = pda_type_idents + // Generate match arms that extract idx fields, resolve Pubkeys, construct CtxSeeds + let pda_match_arms: Vec<_> = pda_ctx_seeds .iter() - .map(|pda_type| { + .map(|info| { + let pda_type = &info.type_name; let packed_name = format_ident!("Packed{}", pda_type); - quote! { - CompressedAccountVariant::#packed_name(packed) => { - match light_sdk::compressible::handle_packed_pda_variant::<#pda_type, #packed_name, _, _>( - &*self.rent_sponsor, - cpi_accounts, - address_space, - &solana_accounts[i], - i, - &packed, - &meta, - post_system_accounts, - &mut compressed_pda_infos, - &program_id, - self, // Pass the context itself as seed_accounts - seed_params, - ) { - std::result::Result::Ok(()) => {}, - std::result::Result::Err(e) => return std::result::Result::Err(e), + let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", pda_type); + let ctx_fields = &info.ctx_seed_fields; + + // Generate pattern to extract idx fields from packed variant + let idx_field_patterns: Vec<_> = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { #idx_field } + }).collect(); + + // Generate code to resolve idx fields to Pubkeys + let resolve_ctx_seeds: Vec<_> = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { + let #field = *post_system_accounts + .get(#idx_field as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key; + } + }).collect(); + + // Generate CtxSeeds struct construction + let ctx_seeds_construction = if ctx_fields.is_empty() { + quote! { let ctx_seeds = #ctx_seeds_struct_name; } + } else { + let field_inits: Vec<_> = ctx_fields.iter().map(|field| { + quote! { #field } + }).collect(); + quote! { let ctx_seeds = #ctx_seeds_struct_name { #(#field_inits),* }; } + }; + + if ctx_fields.is_empty() { + quote! { + RentFreeAccountVariant::#packed_name { data: packed, .. } => { + #ctx_seeds_construction + match light_sdk::compressible::handle_packed_pda_variant::<#pda_type, #packed_name, _, _>( + &*self.rent_sponsor, + cpi_accounts, + address_space, + &solana_accounts[i], + i, + &packed, + &meta, + post_system_accounts, + &mut compressed_pda_infos, + &program_id, + &ctx_seeds, + seed_params, + ) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(e) => return std::result::Result::Err(e), + } + } + RentFreeAccountVariant::#pda_type { .. } => { + unreachable!("Unpacked variants should not be present during decompression"); } } - CompressedAccountVariant::#pda_type(_) => { - unreachable!("Unpacked variants should not be present during decompression"); + } else { + quote! { + RentFreeAccountVariant::#packed_name { data: packed, #(#idx_field_patterns,)* .. } => { + #(#resolve_ctx_seeds)* + #ctx_seeds_construction + match light_sdk::compressible::handle_packed_pda_variant::<#pda_type, #packed_name, _, _>( + &*self.rent_sponsor, + cpi_accounts, + address_space, + &solana_accounts[i], + i, + &packed, + &meta, + post_system_accounts, + &mut compressed_pda_infos, + &program_id, + &ctx_seeds, + seed_params, + ) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(e) => return std::result::Result::Err(e), + } + } + RentFreeAccountVariant::#pda_type { .. } => { + unreachable!("Unpacked variants should not be present during decompression"); + } } } }) .collect(); + let packed_token_variant_ident = format_ident!("Packed{}", token_variant_ident); + Ok(quote! { impl<#lifetime> light_sdk::compressible::DecompressContext<#lifetime> for DecompressAccountsIdempotent<#lifetime> { +<<<<<<< HEAD type CompressedData = CompressedAccountData; type PackedTokenData = light_token_sdk::compat::PackedCTokenData<#token_variant_ident>; +======= + type CompressedData = RentFreeAccountData; + type PackedTokenData = light_ctoken_sdk::compat::PackedCTokenData<#packed_token_variant_ident>; +>>>>>>> a606eb113 (wip) type CompressedMeta = light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; - type SeedParams = SeedParams; + type SeedParams = (); fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> { &*self.fee_payer @@ -126,11 +170,11 @@ pub fn generate_decompress_context_trait_impl( let meta = compressed_data.meta; match compressed_data.data { #(#pda_match_arms)* - CompressedAccountVariant::PackedCTokenData(mut data) => { + RentFreeAccountVariant::PackedCTokenData(mut data) => { data.token_data.version = 3; compressed_token_accounts.push((data, meta)); } - CompressedAccountVariant::CTokenData(_) => { + RentFreeAccountVariant::CTokenData(_) => { unreachable!(); } } @@ -154,10 +198,14 @@ pub fn generate_decompress_context_trait_impl( proof: light_sdk::instruction::ValidityProof, cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, #lifetime>, post_system_accounts: &[solana_account_info::AccountInfo<#lifetime>], - has_pdas: bool, + has_prior_context: bool, ) -> std::result::Result<(), solana_program_error::ProgramError> { +<<<<<<< HEAD light_token_sdk::compressible::process_decompress_tokens_runtime( self, +======= + light_ctoken_sdk::compressible::process_decompress_tokens_runtime( +>>>>>>> a606eb113 (wip) remaining_accounts, fee_payer, token_program, @@ -169,51 +217,10 @@ pub fn generate_decompress_context_trait_impl( proof, cpi_accounts, post_system_accounts, - has_pdas, + has_prior_context, &crate::ID, ) } } }) } - -pub fn derive_decompress_context(input: DeriveInput) -> Result { - let pda_types_attr = input - .attrs - .iter() - .find(|attr| attr.path().is_ident("pda_types")) - .ok_or_else(|| { - syn::Error::new_spanned( - &input, - "DecompressContext derive requires #[pda_types(Type1, Type2, ...)] attribute", - ) - })?; - - let pda_types: PdaTypesAttr = pda_types_attr.parse_args()?; - let pda_type_idents: Vec = pda_types.types.iter().cloned().collect(); - - let token_variant_attr = input - .attrs - .iter() - .find(|attr| attr.path().is_ident("token_variant")) - .ok_or_else(|| { - syn::Error::new_spanned( - &input, - "DecompressContext derive requires #[token_variant(CTokenAccountVariant)] attribute", - ) - })?; - - let token_variant: TokenVariantAttr = token_variant_attr.parse_args()?; - let token_variant_ident = token_variant.variant; - - let lifetime = if let Some(lt) = input.generics.lifetimes().next() { - lt.lifetime.clone() - } else { - return Err(syn::Error::new_spanned( - &input, - "DecompressContext requires a lifetime parameter (e.g., <'info>)", - )); - }; - - generate_decompress_context_trait_impl(pda_type_idents, token_variant_ident, lifetime) -} diff --git a/sdk-libs/macros/src/compressible/file_scanner.rs b/sdk-libs/macros/src/compressible/file_scanner.rs new file mode 100644 index 0000000000..732ff11060 --- /dev/null +++ b/sdk-libs/macros/src/compressible/file_scanner.rs @@ -0,0 +1,169 @@ +//! File scanning for #[rentfree_program] macro. +//! +//! This module reads external Rust source files to extract seed information +//! from Accounts structs that contain #[rentfree] fields. + +use std::path::{Path, PathBuf}; +use syn::{Item, ItemMod, ItemStruct}; + +use crate::compressible::anchor_seeds::{ + extract_from_accounts_struct, ExtractedAccountsInfo, ExtractedSeedSpec, ExtractedTokenSpec, +}; + +/// Result of scanning a module and its external files +#[derive(Debug, Default)] +pub struct ScannedModuleInfo { + pub pda_specs: Vec, + pub token_specs: Vec, + pub errors: Vec, +} + +/// Scan the entire src/ directory for Accounts structs with #[rentfree] fields. +/// +/// This function scans all .rs files in the crate's src/ directory +/// and extracts seed information from Accounts structs. +pub fn scan_module_for_compressible( + _module: &ItemMod, + base_path: &Path, +) -> syn::Result { + let mut result = ScannedModuleInfo::default(); + + // Scan all .rs files in the src directory + scan_directory_recursive(base_path, &mut result); + + Ok(result) +} + +/// Recursively scan a directory for .rs files +fn scan_directory_recursive(dir: &Path, result: &mut ScannedModuleInfo) { + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(e) => { + result.errors.push(format!("Failed to read directory {:?}: {}", dir, e)); + return; + } + }; + + for entry in entries.flatten() { + let path = entry.path(); + + if path.is_dir() { + scan_directory_recursive(&path, result); + } else if path.extension().map(|e| e == "rs").unwrap_or(false) { + scan_rust_file(&path, result); + } + } +} + +/// Scan a single Rust file for Accounts structs +fn scan_rust_file(path: &Path, result: &mut ScannedModuleInfo) { + let contents = match std::fs::read_to_string(path) { + Ok(c) => c, + Err(e) => { + result.errors.push(format!("Failed to read {:?}: {}", path, e)); + return; + } + }; + + let parsed: syn::File = match syn::parse_str(&contents) { + Ok(f) => f, + Err(e) => { + // Not all files may be valid on their own (e.g., test files with main) + // Just skip them silently + let _ = e; + return; + } + }; + + for item in parsed.items { + match item { + Item::Struct(item_struct) => { + if let Ok(Some(info)) = try_extract_from_struct(&item_struct) { + result.pda_specs.extend(info.pda_fields); + result.token_specs.extend(info.token_fields); + } + } + Item::Mod(inner_mod) if inner_mod.content.is_some() => { + // Inline module - recursively scan + scan_inline_module(&inner_mod, result); + } + _ => {} + } + } +} + +/// Scan an inline module for Accounts structs +fn scan_inline_module(module: &ItemMod, result: &mut ScannedModuleInfo) { + let content = match &module.content { + Some((_, items)) => items, + None => return, + }; + + for item in content { + match item { + Item::Struct(item_struct) => { + if let Ok(Some(info)) = try_extract_from_struct(item_struct) { + result.pda_specs.extend(info.pda_fields); + result.token_specs.extend(info.token_fields); + } + } + Item::Mod(inner_mod) if inner_mod.content.is_some() => { + scan_inline_module(inner_mod, result); + } + _ => {} + } + } +} + +/// Try to extract rentfree info from a struct +fn try_extract_from_struct(item_struct: &ItemStruct) -> syn::Result> { + // Check if it has #[derive(Accounts)] + let has_accounts_derive = item_struct.attrs.iter().any(|attr| { + if attr.path().is_ident("derive") { + if let Ok(meta) = attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) { + return meta.iter().any(|p| p.is_ident("Accounts")); + } + } + false + }); + + if !has_accounts_derive { + return Ok(None); + } + + extract_from_accounts_struct(item_struct) +} + +/// Resolve the base path for the crate source +/// +/// This attempts to find the src/ directory by looking at CARGO_MANIFEST_DIR +/// or falling back to current directory. +pub fn resolve_crate_src_path() -> PathBuf { + // Try CARGO_MANIFEST_DIR first (set during cargo build) + if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { + let src_path = PathBuf::from(&manifest_dir).join("src"); + if src_path.exists() { + return src_path; + } + // Fallback to manifest dir itself + return PathBuf::from(manifest_dir); + } + + // Fallback to current directory + std::env::current_dir() + .unwrap_or_else(|_| PathBuf::from(".")) + .join("src") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_resolve_path() { + let path = resolve_crate_src_path(); + println!("Resolved path: {:?}", path); + } +} diff --git a/sdk-libs/macros/src/compressible/instructions.rs b/sdk-libs/macros/src/compressible/instructions.rs index 620b6d225c..7c94029f95 100644 --- a/sdk-libs/macros/src/compressible/instructions.rs +++ b/sdk-libs/macros/src/compressible/instructions.rs @@ -8,6 +8,22 @@ use syn::{ Expr, Ident, Item, ItemMod, LitStr, Result, Token, }; +/// Convert PascalCase to snake_case (e.g., UserRecord -> user_record) +fn to_snake_case(s: &str) -> String { + let mut result = String::new(); + for (i, c) in s.chars().enumerate() { + if c.is_uppercase() { + if i > 0 { + result.push('_'); + } + result.push(c.to_ascii_lowercase()); + } else { + result.push(c); + } + } + result +} + macro_rules! macro_error { ($span:expr, $msg:expr) => { syn::Error::new_spanned( @@ -45,145 +61,100 @@ pub struct TokenSeedSpec { pub variant: Ident, pub _eq: Token![=], pub is_token: Option, - pub is_ata: bool, pub seeds: Punctuated, pub authority: Option>, } impl Parse for TokenSeedSpec { fn parse(input: ParseStream) -> Result { - let variant = input.parse()?; - let _eq = input.parse()?; + let variant: Ident = input.parse()?; + let _eq: Token![=] = input.parse()?; let content; syn::parenthesized!(content in input); - let (is_token, is_ata, seeds, authority) = if content.peek(Ident) { - let first_ident: Ident = content.parse()?; - - match first_ident.to_string().as_str() { - "is_token" => { - let _comma: Token![,] = content.parse()?; - - if content.peek(Ident) { - let fork = content.fork(); - if let Ok(second_ident) = fork.parse::() { - if second_ident == "is_ata" { - let _: Ident = content.parse()?; - return Ok(TokenSeedSpec { - variant, - _eq, - is_token: Some(true), - is_ata: true, - seeds: Punctuated::new(), - authority: None, - }); - } - } + // New explicit syntax: + // PDA: TypeName = (seeds = (...)) + // Token: TypeName = (is_token, seeds = (...), authority = (...)) + let mut is_token = None; + let mut seeds = Punctuated::new(); + let mut authority = None; + + while !content.is_empty() { + if content.peek(Ident) { + let ident: Ident = content.parse()?; + let ident_str = ident.to_string(); + + match ident_str.as_str() { + "is_token" | "true" => { + is_token = Some(true); } - - let (seeds, authority) = parse_seeds_with_authority(&content)?; - (Some(true), false, seeds, authority) - } - "true" => { - let _comma: Token![,] = content.parse()?; - let (seeds, authority) = parse_seeds_with_authority(&content)?; - (Some(true), false, seeds, authority) - } - "is_pda" | "false" => { - let _comma: Token![,] = content.parse()?; - let (seeds, authority) = parse_seeds_with_authority(&content)?; - (Some(false), false, seeds, authority) - } - _ => { - let mut seeds = Punctuated::new(); - // Allow function-call expressions starting with an identifier, e.g. max_key(...) - if content.peek(syn::token::Paren) { - let args_tokens; - syn::parenthesized!(args_tokens in content); - let inner_ts: proc_macro2::TokenStream = args_tokens.parse()?; - let call_expr: syn::Expr = - syn::parse2(quote! { #first_ident( #inner_ts ) })?; - seeds.push(SeedElement::Expression(Box::new(call_expr))); - } else { - seeds.push(SeedElement::Expression(Box::new(syn::Expr::Path( - syn::ExprPath { - attrs: vec![], - qself: None, - path: syn::Path::from(first_ident), - }, - )))); + "is_pda" | "false" => { + is_token = Some(false); } - - if content.peek(Token![,]) { - let _comma: Token![,] = content.parse()?; - let (rest, authority) = parse_seeds_with_authority(&content)?; - seeds.extend(rest); - (None, false, seeds, authority) - } else { - (None, false, seeds, None) + "seeds" => { + let _eq: Token![=] = content.parse()?; + let seeds_content; + syn::parenthesized!(seeds_content in content); + seeds = parse_seed_elements(&seeds_content)?; + } + "authority" => { + let _eq: Token![=] = content.parse()?; + authority = Some(parse_authority_seeds(&content)?); + } + _ => { + return Err(syn::Error::new_spanned( + &ident, + format!( + "Unknown keyword '{}'. Expected: is_token, seeds, or authority.\n\ + Use explicit syntax: TypeName = (seeds = (\"seed\", ctx.account, ...))\n\ + For tokens: TypeName = (is_token, seeds = (...), authority = (...))", + ident_str + ), + )); } } + } else { + return Err(syn::Error::new( + content.span(), + "Expected keyword (is_token, seeds, or authority). Use explicit syntax:\n\ + - PDA: TypeName = (seeds = (\"seed\", ctx.account, ...))\n\ + - Token: TypeName = (is_token, seeds = (...), authority = (...))", + )); } - } else { - let (seeds, authority) = parse_seeds_with_authority(&content)?; - (None, false, seeds, authority) - }; + + if content.peek(Token![,]) { + let _comma: Token![,] = content.parse()?; + } else { + break; + } + } + + if seeds.is_empty() { + return Err(syn::Error::new_spanned( + &variant, + format!( + "Missing seeds for '{}'. Use: {} = (seeds = (\"seed\", ctx.account, ...))", + variant, variant + ), + )); + } Ok(TokenSeedSpec { variant, _eq, is_token, - is_ata, seeds, authority, }) } } -#[allow(clippy::type_complexity)] -fn parse_seeds_with_authority( - content: ParseStream, -) -> Result<(Punctuated, Option>)> { +/// Parse seed elements from within seeds = (...) +fn parse_seed_elements(content: ParseStream) -> Result> { let mut seeds = Punctuated::new(); - let mut authority = None; while !content.is_empty() { - if content.peek(Ident) { - let fork = content.fork(); - if let Ok(ident) = fork.parse::() { - if ident == "authority" && fork.peek(Token![=]) { - let _: Ident = content.parse()?; - let _: Token![=] = content.parse()?; - - if content.peek(syn::token::Paren) { - let auth_content; - syn::parenthesized!(auth_content in content); - let mut auth_seeds = Vec::new(); - - while !auth_content.is_empty() { - auth_seeds.push(auth_content.parse::()?); - if auth_content.peek(Token![,]) { - let _: Token![,] = auth_content.parse()?; - } else { - break; - } - } - authority = Some(auth_seeds); - } else { - authority = Some(vec![content.parse::()?]); - } - - if content.peek(Token![,]) { - let _: Token![,] = content.parse()?; - continue; - } else { - break; - } - } - } - } - seeds.push(content.parse::()?); if content.peek(Token![,]) { @@ -196,7 +167,29 @@ fn parse_seeds_with_authority( } } - Ok((seeds, authority)) + Ok(seeds) +} + +/// Parse authority seeds - either parenthesized tuple or single expression +fn parse_authority_seeds(content: ParseStream) -> Result> { + if content.peek(syn::token::Paren) { + let auth_content; + syn::parenthesized!(auth_content in content); + let mut auth_seeds = Vec::new(); + + while !auth_content.is_empty() { + auth_seeds.push(auth_content.parse::()?); + if auth_content.peek(Token![,]) { + let _: Token![,] = auth_content.parse()?; + } else { + break; + } + } + Ok(auth_seeds) + } else { + // Single expression (e.g., LIGHT_CPI_SIGNER) + Ok(vec![content.parse::()?]) + } } #[derive(Clone)] @@ -215,6 +208,142 @@ impl Parse for SeedElement { } } +/// Recursively extract all ctx.XXX or ctx.accounts.XXX field names from an expression. +/// Handles nested expressions like function calls: max_key(&ctx.user.key(), &ctx.authority.key()) +fn extract_ctx_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { + match expr { + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + // Check for ctx.XXX pattern (direct field access) + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + fields.push(field_name.clone()); + return; + } + } + } + // Check for ctx.accounts.XXX pattern (nested field access) + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + fields.push(field_name.clone()); + return; + } + } + } + } + } + } + } + // Recurse into base expression + extract_ctx_fields_from_expr(&field_expr.base, fields); + } + syn::Expr::MethodCall(method) => { + // Recurse into receiver and args + extract_ctx_fields_from_expr(&method.receiver, fields); + for arg in &method.args { + extract_ctx_fields_from_expr(arg, fields); + } + } + syn::Expr::Call(call) => { + // Recurse into function args + for arg in &call.args { + extract_ctx_fields_from_expr(arg, fields); + } + } + syn::Expr::Reference(ref_expr) => { + extract_ctx_fields_from_expr(&ref_expr.expr, fields); + } + syn::Expr::Paren(paren) => { + extract_ctx_fields_from_expr(&paren.expr, fields); + } + _ => {} + } +} + +/// Extract ctx.XXX or ctx.accounts.XXX field names from a seed element. +fn extract_ctx_account_fields(seed: &SeedElement) -> Vec { + let mut fields = Vec::new(); + if let SeedElement::Expression(expr) = seed { + extract_ctx_fields_from_expr(expr, &mut fields); + } + fields +} + +/// Extract all ctx.accounts.XXX field names from a list of seed elements. +/// Deduplicates the fields. +pub fn extract_ctx_seed_fields(seeds: &syn::punctuated::Punctuated) -> Vec { + let mut all_fields = Vec::new(); + for seed in seeds { + all_fields.extend(extract_ctx_account_fields(seed)); + } + // Deduplicate while preserving order + let mut seen = std::collections::HashSet::new(); + all_fields + .into_iter() + .filter(|f| seen.insert(f.to_string())) + .collect() +} + +/// Phase 5: Extract data.XXX field names from an expression recursively. +fn extract_data_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { + match expr { + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + // Check for data.XXX pattern + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + fields.push(field_name.clone()); + return; + } + } + } + } + // Recurse into base expression + extract_data_fields_from_expr(&field_expr.base, fields); + } + syn::Expr::MethodCall(method) => { + extract_data_fields_from_expr(&method.receiver, fields); + for arg in &method.args { + extract_data_fields_from_expr(arg, fields); + } + } + syn::Expr::Call(call) => { + for arg in &call.args { + extract_data_fields_from_expr(arg, fields); + } + } + syn::Expr::Reference(ref_expr) => { + extract_data_fields_from_expr(&ref_expr.expr, fields); + } + syn::Expr::Paren(paren) => { + extract_data_fields_from_expr(&paren.expr, fields); + } + _ => {} + } +} + +/// Phase 5: Extract all data.XXX field names from a list of seed elements. +pub fn extract_data_seed_fields(seeds: &syn::punctuated::Punctuated) -> Vec { + let mut all_fields = Vec::new(); + for seed in seeds { + if let SeedElement::Expression(expr) = seed { + extract_data_fields_from_expr(expr, &mut all_fields); + } + } + // Deduplicate while preserving order + let mut seen = std::collections::HashSet::new(); + all_fields + .into_iter() + .filter(|f| seen.insert(f.to_string())) + .collect() +} + pub struct InstructionDataSpec { pub field_name: Ident, pub field_type: syn::Type, @@ -233,6 +362,7 @@ impl Parse for InstructionDataSpec { } } +<<<<<<< HEAD struct EnhancedMacroArgs { account_types: Vec, pda_seeds: Vec, @@ -882,16 +1012,18 @@ pub fn add_compressible_instructions( }) } +======= +>>>>>>> a606eb113 (wip) pub fn generate_decompress_context_impl( _variant: InstructionVariant, - pda_type_idents: Vec, + pda_ctx_seeds: Vec, token_variant_ident: Ident, ) -> Result { let lifetime: syn::Lifetime = syn::parse_quote!('info); let trait_impl = crate::compressible::decompress_context::generate_decompress_context_trait_impl( - pda_type_idents, + pda_ctx_seeds, token_variant_ident, lifetime, )?; @@ -907,27 +1039,17 @@ pub fn generate_decompress_context_impl( pub fn generate_process_decompress_accounts_idempotent( _variant: InstructionVariant, - instruction_data: &[InstructionDataSpec], + _instruction_data: &[InstructionDataSpec], ) -> Result { - // If we have seed parameters, accept them as a single struct - let (params, seed_params_arg) = if !instruction_data.is_empty() { - ( - quote! { seed_data: SeedParams, }, - quote! { std::option::Option::Some(&seed_data) }, - ) - } else { - (quote! {}, quote! { std::option::Option::None }) - }; - + // Phase 4: seed_data removed - data.* seeds come from unpacked account data, ctx.* from variant idx 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, + compressed_accounts: Vec, system_accounts_offset: u8, - #params ) -> Result<()> { light_sdk::compressible::process_decompress_accounts_idempotent( accounts, @@ -937,7 +1059,7 @@ pub fn generate_process_decompress_accounts_idempotent( system_accounts_offset, LIGHT_CPI_SIGNER, &crate::ID, - #seed_params_arg, + std::option::Option::None::<&()>, ) .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) } @@ -946,23 +1068,17 @@ pub fn generate_process_decompress_accounts_idempotent( pub fn generate_decompress_instruction_entrypoint( _variant: InstructionVariant, - instruction_data: &[InstructionDataSpec], + _instruction_data: &[InstructionDataSpec], ) -> Result { - // If we have seed parameters, pass them as a single struct - let (params, args) = if !instruction_data.is_empty() { - (quote! { seed_data: SeedParams, }, quote! { seed_data, }) - } else { - (quote! {}, quote! {}) - }; + // Phase 4: seed_data removed - data.* seeds come from unpacked account data, ctx.* from variant idx 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, + compressed_accounts: Vec, system_accounts_offset: u8, - #params ) -> Result<()> { __processor_functions::process_decompress_accounts_idempotent( &ctx.accounts, @@ -970,7 +1086,6 @@ pub fn generate_decompress_instruction_entrypoint( proof, compressed_accounts, system_accounts_offset, - #args ) } }) @@ -1014,60 +1129,8 @@ pub fn generate_compress_context_impl( cpi_accounts, &compression_config.address_space, )?; - // Compute rent-based close distribution and transfer lamports: - // - Completed epochs to rent sponsor - // - Partial epoch (unused) to fee payer (user refund) - #[cfg(target_os = "solana")] - let current_slot = anchor_lang::solana_program::sysvar::clock::Clock::get() - .map_err(|_| anchor_lang::prelude::ProgramError::UnsupportedSysvar)? - .slot; - #[cfg(not(target_os = "solana"))] - let current_slot = 0; - let bytes = account_info.data_len() as u64; - let current_lamports = account_info.lamports(); - let rent_exemption = anchor_lang::solana_program::sysvar::rent::Rent::get() - .map_err(|_| anchor_lang::prelude::ProgramError::UnsupportedSysvar)? - .minimum_balance(bytes as usize); - let ci_ref = account_data.compression_info(); - let state = light_compressible::rent::AccountRentState { - num_bytes: bytes, - current_slot, - current_lamports, - last_claimed_slot: ci_ref.last_claimed_slot(), - }; - let dist = state.calculate_close_distribution(&ci_ref.rent_config, rent_exemption); - // Transfer partial epoch back to fee payer (user) - if dist.to_user > 0 { - let fee_payer_info = self.fee_payer.to_account_info(); - let mut src = account_info.try_borrow_mut_lamports().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - program_error - })?; - let mut dst = fee_payer_info.try_borrow_mut_lamports().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - program_error - })?; - **src = src.checked_sub(dist.to_user).ok_or(anchor_lang::prelude::ProgramError::InsufficientFunds)?; - **dst = dst.checked_add(dist.to_user).ok_or(anchor_lang::prelude::ProgramError::Custom(0))?; - } - // Transfer completed epochs (and base) to rent sponsor - if dist.to_rent_sponsor > 0 { - let rent_sponsor_info = self.rent_sponsor.to_account_info(); - let mut src = account_info.try_borrow_mut_lamports().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - program_error - })?; - let mut dst = rent_sponsor_info.try_borrow_mut_lamports().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - program_error - })?; - **src = src.checked_sub(dist.to_rent_sponsor).ok_or(anchor_lang::prelude::ProgramError::InsufficientFunds)?; - **dst = dst.checked_add(dist.to_rent_sponsor).ok_or(anchor_lang::prelude::ProgramError::Custom(0))?; - } + // Lamport transfers are handled by close() in process_compress_pda_accounts_idempotent + // All lamports go to rent_sponsor for simplicity Ok(Some(compressed_info)) } } @@ -1179,34 +1242,41 @@ pub fn generate_compress_instruction_entrypoint( }) } +/// Phase 3: Generate PDA seed derivation that uses CtxSeeds struct instead of DecompressAccountsIdempotent. +/// Maps ctx.field -> ctx_seeds.field (direct Pubkey access, no Option unwrapping needed) #[inline(never)] -fn generate_pda_seed_derivation_for_trait( +fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( spec: &TokenSeedSpec, _instruction_data: &[InstructionDataSpec], + ctx_seed_fields: &[syn::Ident], ) -> Result { let mut bindings: Vec = Vec::new(); let mut seed_refs = Vec::new(); + // Convert ctx_seed_fields to a set for quick lookup + let ctx_field_names: std::collections::HashSet = + ctx_seed_fields.iter().map(|f| f.to_string()).collect(); + // Recursively rewrite expressions: - // - `data.` -> `seed_params.` (from instruction params, not struct fields!) - // - `ctx.accounts.` -> `accounts.` - // - `ctx.` -> `accounts.` - // While preserving function/method calls and references. - fn map_pda_expr_to_params(expr: &syn::Expr) -> syn::Expr { + // - `data.` -> `self.` (from unpacked compressed account data - Phase 4) + // - `ctx.accounts.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) + // - `ctx.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) + fn map_pda_expr_to_ctx_seeds( + expr: &syn::Expr, + ctx_field_names: &std::collections::HashSet, + ) -> syn::Expr { match expr { syn::Expr::Field(field_expr) => { if let syn::Member::Named(field_name) = &field_expr.member { - // Handle nested field access: ctx.accounts.field_name -> accounts.field_name.as_ref().ok_or(...)?.key() + // Handle nested field access: ctx.accounts.field_name -> ctx_seeds.field_name if let syn::Expr::Field(nested_field) = &*field_expr.base { if let syn::Member::Named(base_name) = &nested_field.member { if base_name == "accounts" { if let syn::Expr::Path(path) = &*nested_field.base { if let Some(segment) = path.path.segments.first() { if segment.ident == "ctx" { - return syn::parse_quote! { accounts.#field_name.as_ref().ok_or_else(|| -> solana_program_error::ProgramError { - let err: anchor_lang::error::Error = CompressibleInstructionError::MissingSeedAccount.into(); - err.into() - })?.key() }; + // ctx.accounts.field -> ctx_seeds.field (direct Pubkey) + return syn::parse_quote! { ctx_seeds.#field_name }; } } } @@ -1217,14 +1287,14 @@ fn generate_pda_seed_derivation_for_trait( if let syn::Expr::Path(path) = &*field_expr.base { if let Some(segment) = path.path.segments.first() { if segment.ident == "data" { - // data.field -> seed_params.field (from instruction params!) - return syn::parse_quote! { seed_params.#field_name }; + // Phase 4: data.field -> self.field (from unpacked compressed account data) + return syn::parse_quote! { self.#field_name }; } else if segment.ident == "ctx" { - // ctx.field -> accounts.field.as_ref().ok_or(...)?.key() (error if optional account is missing) - return syn::parse_quote! { accounts.#field_name.as_ref().ok_or_else(|| -> solana_program_error::ProgramError { - let err: anchor_lang::error::Error = CompressibleInstructionError::MissingSeedAccount.into(); - err.into() - })?.key() }; + let field_str = field_name.to_string(); + if ctx_field_names.contains(&field_str) { + // ctx.field -> ctx_seeds.field (direct Pubkey) + return syn::parse_quote! { ctx_seeds.#field_name }; + } } } } @@ -1232,32 +1302,32 @@ fn generate_pda_seed_derivation_for_trait( expr.clone() } syn::Expr::MethodCall(method_call) => { - // Special case: ctx.accounts.account_name.key() -> accounts.account_name.key() - // This is already handled by the Field case transforming ctx.accounts.X -> accounts.X let mut new_method_call = method_call.clone(); - new_method_call.receiver = Box::new(map_pda_expr_to_params(&method_call.receiver)); + new_method_call.receiver = + Box::new(map_pda_expr_to_ctx_seeds(&method_call.receiver, ctx_field_names)); new_method_call.args = method_call .args .iter() - .map(map_pda_expr_to_params) + .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) .collect(); syn::Expr::MethodCall(new_method_call) } syn::Expr::Call(call_expr) => { - // Map function args recursively. We do not transform the function path. let mut new_call_expr = call_expr.clone(); - new_call_expr.args = call_expr.args.iter().map(map_pda_expr_to_params).collect(); + new_call_expr.args = call_expr + .args + .iter() + .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) + .collect(); syn::Expr::Call(new_call_expr) } syn::Expr::Reference(ref_expr) => { let mut new_ref_expr = ref_expr.clone(); - new_ref_expr.expr = Box::new(map_pda_expr_to_params(&ref_expr.expr)); + new_ref_expr.expr = + Box::new(map_pda_expr_to_ctx_seeds(&ref_expr.expr, ctx_field_names)); syn::Expr::Reference(new_ref_expr) } - _ => { - // For other expressions (constants, literals, paths), leave as-is - expr.clone() - } + _ => expr.clone(), } } @@ -1268,22 +1338,31 @@ fn generate_pda_seed_derivation_for_trait( seed_refs.push(quote! { #value.as_bytes() }); } SeedElement::Expression(expr) => { + // Handle byte string literals: b"seed" -> use directly (no .as_bytes()) + if let syn::Expr::Lit(lit_expr) = &**expr { + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + seed_refs.push(quote! { &[#(#bytes),*] }); + continue; + } + } + + // Handle uppercase constants if let syn::Expr::Path(path_expr) = &**expr { if let Some(ident) = path_expr.path.get_ident() { let ident_str = ident.to_string(); if ident_str.chars().all(|c| c.is_uppercase() || c == '_') { - seed_refs.push(quote! { #ident.as_bytes() }); + seed_refs.push( + quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }, + ); continue; } } } - // Generic solution: rewrite any `data.*` occurrences recursively to `self.*`, - // then bind the result to a local to ensure lifetimes are valid, - // and use `.as_ref()` to convert into a seed `&[u8]`. let binding_name = syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); - let mapped_expr = map_pda_expr_to_params(expr); + let mapped_expr = map_pda_expr_to_ctx_seeds(expr, &ctx_field_names); bindings.push(quote! { let #binding_name = #mapped_expr; }); @@ -1307,127 +1386,6 @@ fn generate_pda_seed_derivation_for_trait( }) } -#[inline(never)] -fn extract_required_accounts_from_seeds( - pda_seeds: &Option>, - token_seeds: &Option>, -) -> Result> { - let mut required_accounts: Vec = Vec::new(); - - #[inline(always)] - fn push_unique(list: &mut Vec, value: String) { - if !list.iter().any(|v| v == &value) { - list.push(value); - } - } - - #[inline(never)] - fn extract_accounts_from_seed_spec( - spec: &TokenSeedSpec, - ordered_accounts: &mut Vec, - ) -> Result> { - let mut spec_accounts = Vec::new(); - for seed in &spec.seeds { - if let SeedElement::Expression(expr) = seed { - let mut local_accounts = Vec::new(); - extract_account_from_expr(expr, &mut local_accounts); - for acc in local_accounts { - push_unique(ordered_accounts, acc.clone()); - push_unique(&mut spec_accounts, acc); - } - } - } - if let Some(authority_seeds) = &spec.authority { - for seed in authority_seeds { - if let SeedElement::Expression(expr) = seed { - let mut local_accounts = Vec::new(); - extract_account_from_expr(expr, &mut local_accounts); - for acc in local_accounts { - push_unique(ordered_accounts, acc.clone()); - push_unique(&mut spec_accounts, acc); - } - } - } - } - Ok(spec_accounts) - } - - if let Some(pda_seed_specs) = pda_seeds { - for spec in pda_seed_specs { - let _required_seeds = extract_accounts_from_seed_spec(spec, &mut required_accounts)?; - } - } - - if let Some(token_seed_specs) = token_seeds { - for spec in token_seed_specs { - let _required_seeds = extract_accounts_from_seed_spec(spec, &mut required_accounts)?; - } - } - - Ok(required_accounts) -} - -#[inline(never)] -fn extract_account_from_expr(expr: &syn::Expr, ordered_accounts: &mut Vec) { - #[inline(always)] - fn push_unique(list: &mut Vec, value: String) { - if !list.iter().any(|v| v == &value) { - list.push(value); - } - } - - match expr { - syn::Expr::MethodCall(method_call) => { - extract_account_from_expr(&method_call.receiver, ordered_accounts); - } - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - push_unique(ordered_accounts, field_name.to_string()); - } - } - } - } - } - } else if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" && field_name != "accounts" { - push_unique(ordered_accounts, field_name.to_string()); - } - } - } - } - } - syn::Expr::Path(path_expr) => { - if let Some(ident) = path_expr.path.get_ident() { - let name = ident.to_string(); - if name != "ctx" - && name != "data" - && !name - .chars() - .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) - { - push_unique(ordered_accounts, name); - } - } - } - syn::Expr::Call(call_expr) => { - for arg in &call_expr.args { - extract_account_from_expr(arg, ordered_accounts); - } - } - syn::Expr::Reference(ref_expr) => { - extract_account_from_expr(&ref_expr.expr, ordered_accounts); - } - _ => {} - } -} - #[inline(never)] fn generate_decompress_accounts_struct( required_accounts: &[String], @@ -1507,8 +1465,10 @@ fn generate_decompress_accounts_struct( for account_name in required_accounts { if !standard_fields.contains(&account_name.as_str()) { let account_ident = syn::Ident::new(account_name, proc_macro2::Span::call_site()); + // Mark seed accounts as writable to support CPI calls that may need them writable account_fields.push(quote! { - /// CHECK: optional seed account + /// CHECK: optional seed account - may be used in CPIs + #[account(mut)] pub #account_ident: Option> }); } @@ -1549,8 +1509,8 @@ fn generate_error_codes(variant: InstructionVariant) -> Result { InvalidRentSponsor, #[msg("Missing seed account")] MissingSeedAccount, - #[msg("ATA uses SPL ATA derivation")] - AtaDoesNotUseSeedDerivation, + #[msg("Seed value does not match account data")] + SeedMismatch, }; let variant_specific_errors = match variant { @@ -1576,3 +1536,605 @@ fn generate_error_codes(variant: InstructionVariant) -> Result { } }) } + +/// Convert ClassifiedSeed to SeedElement (Punctuated) +fn convert_classified_to_seed_elements( + seeds: &[crate::compressible::anchor_seeds::ClassifiedSeed], +) -> Punctuated { + use crate::compressible::anchor_seeds::ClassifiedSeed; + + let mut result = Punctuated::new(); + for seed in seeds { + let elem = match seed { + ClassifiedSeed::Literal(bytes) => { + // Convert to string literal + if let Ok(s) = std::str::from_utf8(bytes) { + SeedElement::Literal(syn::LitStr::new(s, proc_macro2::Span::call_site())) + } else { + // Byte array - use expression + let byte_values: Vec<_> = bytes.iter().map(|b| quote!(#b)).collect(); + let expr: Expr = syn::parse_quote!(&[#(#byte_values),*]); + SeedElement::Expression(Box::new(expr)) + } + } + ClassifiedSeed::Constant(path) => { + let expr: Expr = syn::parse_quote!(#path); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::CtxAccount(ident) => { + let expr: Expr = syn::parse_quote!(ctx.#ident); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::DataField { field_name, conversion: None } => { + let expr: Expr = syn::parse_quote!(data.#field_name); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::DataField { field_name, conversion: Some(method) } => { + let expr: Expr = syn::parse_quote!(data.#field_name.#method()); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::FunctionCall { func, ctx_args } => { + let args: Vec = ctx_args.iter().map(|arg| { + syn::parse_quote!(&ctx.#arg.key()) + }).collect(); + let expr: Expr = syn::parse_quote!(#func(#(#args),*)); + SeedElement::Expression(Box::new(expr)) + } + }; + result.push(elem); + } + result +} + +fn convert_classified_to_seed_elements_vec( + seeds: &[crate::compressible::anchor_seeds::ClassifiedSeed], +) -> Vec { + convert_classified_to_seed_elements(seeds).into_iter().collect() +} + +/// Generate all code from extracted seeds (shared logic with add_compressible_instructions) +#[inline(never)] +fn generate_from_extracted_seeds( + module: &mut ItemMod, + account_types: Vec, + pda_seeds: Option>, + token_seeds: Option>, + instruction_data: Vec, +) -> Result { + let size_validation_checks = validate_compressed_account_sizes(&account_types)?; + + let content = module.content.as_mut().unwrap(); + let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { + if !token_seed_specs.is_empty() { + crate::compressible::seed_providers::generate_ctoken_account_variant_enum( + token_seed_specs, + )? + } else { + crate::compressible::utils::generate_empty_ctoken_enum() + } + } else { + crate::compressible::utils::generate_empty_ctoken_enum() + }; + + if let Some(ref token_seed_specs) = token_seeds { + for spec in token_seed_specs { + if spec.authority.is_none() { + return Err(macro_error!( + &spec.variant, + "Token account '{}' must specify authority = for compression signing.", + spec.variant + )); + } + } + } + + let pda_ctx_seeds: Vec = pda_seeds + .as_ref() + .map(|specs| { + specs + .iter() + .map(|spec| { + let ctx_fields = extract_ctx_seed_fields(&spec.seeds); + crate::compressible::variant_enum::PdaCtxSeedInfo::new( + spec.variant.clone(), + ctx_fields, + ) + }) + .collect() + }) + .unwrap_or_default(); + + let account_type_refs: Vec<&Ident> = account_types.iter().collect(); + let enum_and_traits = + crate::compressible::variant_enum::compressed_account_variant_with_ctx_seeds( + &account_type_refs, + &pda_ctx_seeds, + )?; + + let seed_params_struct = quote! { + #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Clone, Debug, Default)] + pub struct SeedParams; + }; + + let instruction_data_types: std::collections::HashMap = instruction_data + .iter() + .map(|spec| (spec.field_name.to_string(), &spec.field_type)) + .collect(); + + let seeds_structs_and_constructors: Vec = if let Some(ref pda_seed_specs) = pda_seeds { + pda_seed_specs + .iter() + .zip(pda_ctx_seeds.iter()) + .map(|(spec, ctx_info)| { + let type_name = &spec.variant; + let seeds_struct_name = format_ident!("{}Seeds", type_name); + let constructor_name = format_ident!("{}", to_snake_case(&type_name.to_string())); + + let ctx_fields = &ctx_info.ctx_seed_fields; + let ctx_field_decls: Vec<_> = ctx_fields.iter().map(|field| { + quote! { pub #field: solana_pubkey::Pubkey } + }).collect(); + + let data_fields = extract_data_seed_fields(&spec.seeds); + let data_field_decls: Vec<_> = data_fields.iter().filter_map(|field| { + let field_str = field.to_string(); + instruction_data_types.get(&field_str).map(|ty| { + quote! { pub #field: #ty } + }) + }).collect(); + + let data_verifications: Vec<_> = data_fields.iter().map(|field| { + quote! { + if data.#field != seeds.#field { + return std::result::Result::Err(CompressibleInstructionError::SeedMismatch.into()); + } + } + }).collect(); + + quote! { + #[derive(Clone, Debug)] + pub struct #seeds_struct_name { + #(#ctx_field_decls,)* + #(#data_field_decls,)* + } + + impl RentFreeAccountVariant { + pub fn #constructor_name( + account_data: &[u8], + seeds: #seeds_struct_name, + ) -> std::result::Result { + use anchor_lang::AnchorDeserialize; + let data = #type_name::deserialize(&mut &account_data[..])?; + + #(#data_verifications)* + + std::result::Result::Ok(Self::#type_name { + data, + #(#ctx_fields: seeds.#ctx_fields,)* + }) + } + } + + impl light_sdk::compressible::IntoVariant for #seeds_struct_name { + fn into_variant(self, data: &[u8]) -> std::result::Result { + RentFreeAccountVariant::#constructor_name(data, self) + } + } + } + }) + .collect() + } else { + Vec::new() + }; + + let has_pda_seeds = pda_seeds.as_ref().map(|p| !p.is_empty()).unwrap_or(false); + let has_token_seeds = token_seeds.as_ref().map(|t| !t.is_empty()).unwrap_or(false); + + let instruction_variant = match (has_pda_seeds, has_token_seeds) { + (true, true) => InstructionVariant::Mixed, + (true, false) => InstructionVariant::PdaOnly, + (false, true) => InstructionVariant::TokenOnly, + (false, false) => { + return Err(macro_error!( + module, + "At least one PDA or token seed specification must be provided" + )) + } + }; + + let error_codes = generate_error_codes(instruction_variant)?; + let decompress_accounts = generate_decompress_accounts_struct(&[], instruction_variant)?; + + let pda_seed_provider_impls: Result> = account_types + .iter() + .zip(pda_ctx_seeds.iter()) + .map(|(name, ctx_info)| { + let name_str = name.to_string(); + let spec = if let Some(ref pda_seed_specs) = pda_seeds { + pda_seed_specs + .iter() + .find(|s| s.variant == name_str) + .ok_or_else(|| { + macro_error!(name, "No seed specification for account type '{}'", name_str) + })? + } else { + return Err(macro_error!(name, "No seed specifications provided")); + }; + + let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", name); + 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 seed_derivation = generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, &instruction_data, ctx_fields)?; + Ok(quote! { + #ctx_seeds_struct + + impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, ()> for #name { + fn derive_pda_seeds_with_accounts( + &self, + program_id: &solana_pubkey::Pubkey, + ctx_seeds: &#ctx_seeds_struct_name, + _seed_params: &(), + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { + #seed_derivation + } + } + }) + }) + .collect(); + let pda_seed_provider_impls = pda_seed_provider_impls?; + + let trait_impls: syn::ItemMod = syn::parse_quote! { + mod __trait_impls { + use super::*; + + impl light_sdk::compressible::HasTokenVariant for RentFreeAccountData { + fn is_packed_ctoken(&self) -> bool { + matches!(self.data, RentFreeAccountVariant::PackedCTokenData(_)) + } + } + } + }; + + let token_variant_name = format_ident!("CTokenAccountVariant"); + + let decompress_context_impl = generate_decompress_context_impl( + instruction_variant, + pda_ctx_seeds.clone(), + token_variant_name, + )?; + let decompress_processor_fn = generate_process_decompress_accounts_idempotent(instruction_variant, &instruction_data)?; + let decompress_instruction = generate_decompress_instruction_entrypoint(instruction_variant, &instruction_data)?; + + let compress_accounts: syn::ItemStruct = match instruction_variant { + InstructionVariant::PdaOnly => unreachable!(), + InstructionVariant::TokenOnly => unreachable!(), + InstructionVariant::Mixed => 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>, + } + }, + }; + + let compress_context_impl = generate_compress_context_impl(instruction_variant, account_types.clone())?; + let compress_processor_fn = generate_process_compress_accounts_idempotent(instruction_variant)?; + let compress_instruction = generate_compress_instruction_entrypoint(instruction_variant)?; + + let module_tokens = quote! { + mod __processor_functions { + use super::*; + #decompress_processor_fn + #compress_processor_fn + } + }; + let processor_module: syn::ItemMod = syn::parse2(module_tokens)?; + + let init_config_accounts: syn::ItemStruct = syn::parse_quote! { + #[derive(Accounts)] + pub struct InitializeCompressionConfig<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub config: AccountInfo<'info>, + /// CHECK: Checked by SDK + pub program_data: AccountInfo<'info>, + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, + } + }; + + let update_config_accounts: syn::ItemStruct = syn::parse_quote! { + #[derive(Accounts)] + pub struct UpdateCompressionConfig<'info> { + /// CHECK: Checked by SDK + #[account(mut)] + pub config: AccountInfo<'info>, + pub update_authority: Signer<'info>, + } + }; + + let init_config_instruction: syn::ItemFn = syn::parse_quote! { + #[inline(never)] + #[allow(clippy::too_many_arguments)] + pub fn initialize_compression_config<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeCompressionConfig<'info>>, + write_top_up: u32, + rent_sponsor: Pubkey, + compression_authority: Pubkey, + rent_config: light_compressible::rent::RentConfig, + address_space: Vec, + ) -> Result<()> { + light_sdk::compressible::process_initialize_compression_config_checked( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + &ctx.accounts.program_data.to_account_info(), + &rent_sponsor, + &compression_authority, + rent_config, + write_top_up, + address_space, + 0, + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + &crate::ID, + )?; + Ok(()) + } + }; + + let update_config_instruction: syn::ItemFn = syn::parse_quote! { + #[inline(never)] + #[allow(clippy::too_many_arguments)] + pub fn update_compression_config<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateCompressionConfig<'info>>, + new_rent_sponsor: Option, + new_compression_authority: Option, + new_rent_config: Option, + new_write_top_up: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> Result<()> { + light_sdk::compressible::process_update_compression_config( + ctx.accounts.config.as_ref(), + ctx.accounts.update_authority.as_ref(), + new_update_authority.as_ref(), + new_rent_sponsor.as_ref(), + new_compression_authority.as_ref(), + new_rent_config, + new_write_top_up, + new_address_space, + &crate::ID, + )?; + Ok(()) + } + }; + + let client_functions = crate::compressible::seed_providers::generate_client_seed_functions( + &account_types, + &pda_seeds, + &token_seeds, + &instruction_data, + )?; + + // Insert SeedParams struct + let seed_params_item: Item = syn::parse2(seed_params_struct)?; + content.1.push(seed_params_item); + + // Insert XxxSeeds structs and RentFreeAccountVariant constructors + for seeds_tokens in seeds_structs_and_constructors.into_iter() { + let wrapped: syn::File = syn::parse2(seeds_tokens)?; + for item in wrapped.items { + content.1.push(item); + } + } + + content.1.push(Item::Verbatim(size_validation_checks)); + content.1.push(Item::Verbatim(enum_and_traits)); + content.1.push(Item::Verbatim(ctoken_enum)); + content.1.push(Item::Struct(decompress_accounts)); + content.1.push(Item::Mod(trait_impls)); + content.1.push(Item::Mod(decompress_context_impl)); + content.1.push(Item::Mod(processor_module)); + content.1.push(Item::Fn(decompress_instruction)); + content.1.push(Item::Struct(compress_accounts)); + content.1.push(Item::Mod(compress_context_impl)); + content.1.push(Item::Fn(compress_instruction)); + content.1.push(Item::Struct(init_config_accounts)); + content.1.push(Item::Struct(update_config_accounts)); + content.1.push(Item::Fn(init_config_instruction)); + content.1.push(Item::Fn(update_config_instruction)); + + // Add pda seed provider impls + for pda_impl in pda_seed_provider_impls.into_iter() { + let wrapped: syn::File = syn::parse2(pda_impl)?; + for item in wrapped.items { + content.1.push(item); + } + } + + // Add ctoken seed provider impl + if let Some(ref seeds) = token_seeds { + if !seeds.is_empty() { + let impl_code = crate::compressible::seed_providers::generate_ctoken_seed_provider_implementation(seeds)?; + let ctoken_impl: syn::ItemImpl = syn::parse2(impl_code)?; + content.1.push(Item::Impl(ctoken_impl)); + } + } + + // Add error codes + let error_item: syn::ItemEnum = syn::parse2(error_codes)?; + content.1.push(Item::Enum(error_item)); + + // Add client functions (module + pub use statement) + let client_file: syn::File = syn::parse2(client_functions)?; + for item in client_file.items { + content.1.push(item); + } + + Ok(quote! { #module }) +} + +// ============================================================================= +// COMPRESSIBLE_PROGRAM: Auto-discovers seeds from external module files +// ============================================================================= + +/// Main entry point for #[compressible_program] macro. +/// +/// This macro reads external module files to extract seed information from +/// Accounts structs with #[compressible] fields. No explicit type list needed! +/// +/// Usage: +/// ```ignore +/// #[compressible_program] +/// #[program] +/// pub mod my_program { +/// pub mod instruction_accounts; // Macro reads this file! +/// pub mod state; +/// +/// use instruction_accounts::*; +/// use state::*; +/// +/// #[light_instruction] +/// pub fn create_user(ctx: Context, params: Params) -> Result<()> { +/// // ... +/// } +/// } +/// ``` +#[inline(never)] +pub fn compressible_program_impl( + _args: TokenStream, + mut module: ItemMod, +) -> Result { + use crate::compressible::anchor_seeds::get_data_fields; + use crate::compressible::file_scanner::{resolve_crate_src_path, scan_module_for_compressible}; + + if module.content.is_none() { + return Err(macro_error!(&module, "Module must have a body")); + } + + // Resolve the crate's src/ directory + let base_path = resolve_crate_src_path(); + + // Scan the module (and external files) for compressible fields + let scanned = scan_module_for_compressible(&module, &base_path)?; + + // Report any errors from file scanning + if !scanned.errors.is_empty() { + let error_msg = scanned.errors.join("\n"); + return Err(macro_error!( + &module, + "Errors while scanning for rentfree types:\n{}", + error_msg + )); + } + + // Check if we found anything + if scanned.pda_specs.is_empty() && scanned.token_specs.is_empty() { + return Err(macro_error!( + &module, + "No #[rentfree] or #[rentfree_token] fields found in any Accounts struct.\n\ + Ensure your Accounts structs are in modules declared with `pub mod xxx;`" + )); + } + + // Convert extracted specs to the format expected by generate_from_extracted_seeds + let mut found_pda_seeds: Vec = Vec::new(); + let mut found_data_fields: Vec = Vec::new(); + let mut account_types: Vec = Vec::new(); + + for pda in &scanned.pda_specs { + account_types.push(pda.inner_type.clone()); + + let seed_elements = convert_classified_to_seed_elements(&pda.seeds); + + // Extract data field types from seeds + for (field_name, conversion) in get_data_fields(&pda.seeds) { + let field_type: syn::Type = if conversion.is_some() { + syn::parse_quote!(u64) + } else { + syn::parse_quote!(solana_pubkey::Pubkey) + }; + + if !found_data_fields.iter().any(|f| f.field_name == field_name) { + found_data_fields.push(InstructionDataSpec { + field_name, + field_type, + }); + } + } + + found_pda_seeds.push(TokenSeedSpec { + variant: pda.inner_type.clone(), + _eq: syn::parse_quote!(=), + is_token: Some(false), + seeds: seed_elements, + authority: None, + }); + } + + // Convert token specs + let mut found_token_seeds: Vec = Vec::new(); + for token in &scanned.token_specs { + let seed_elements = convert_classified_to_seed_elements(&token.seeds); + let authority_elements = token + .authority_seeds + .as_ref() + .map(|seeds| convert_classified_to_seed_elements_vec(seeds)); + + found_token_seeds.push(TokenSeedSpec { + variant: token.variant_name.clone(), + _eq: syn::parse_quote!(=), + is_token: Some(true), + seeds: seed_elements, + authority: authority_elements, + }); + } + + let pda_seeds = if found_pda_seeds.is_empty() { + None + } else { + Some(found_pda_seeds) + }; + + let token_seeds = if found_token_seeds.is_empty() { + None + } else { + Some(found_token_seeds) + }; + + // Use the shared generation logic + generate_from_extracted_seeds( + &mut module, + account_types, + pda_seeds, + token_seeds, + found_data_fields, + ) +} diff --git a/sdk-libs/macros/src/compressible/light_compressible.rs b/sdk-libs/macros/src/compressible/light_compressible.rs new file mode 100644 index 0000000000..5471c0a7b6 --- /dev/null +++ b/sdk-libs/macros/src/compressible/light_compressible.rs @@ -0,0 +1,272 @@ +//! LightCompressible derive macro - consolidates all required traits for compressible accounts. +//! +//! This macro is equivalent to deriving: +//! - `LightHasherSha` (SHA256 hashing) +//! - `LightDiscriminator` (unique discriminator) +//! - `Compressible` (HasCompressionInfo + CompressAs + Size + CompressedInitSpace) +//! - `CompressiblePack` (Pack + Unpack + Packed struct generation) + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{DeriveInput, Fields, ItemStruct, Result}; + +use crate::{ + compressible::{pack_unpack::derive_compressible_pack, traits::derive_compressible}, + discriminator::discriminator, + hasher::derive_light_hasher_sha, +}; + +/// Derives all required traits for a compressible account. +/// +/// This is a convenience macro that combines: +/// - `LightHasherSha` - SHA256-based DataHasher and ToByteArray implementations (type 3 ShaFlat) +/// - `LightDiscriminator` - Unique 8-byte discriminator for the account type +/// - `Compressible` - HasCompressionInfo, CompressAs, Size, CompressedInitSpace traits +/// - `CompressiblePack` - Pack/Unpack traits with Packed struct generation for Pubkey compression +/// +/// # Example +/// +/// ```ignore +/// use light_sdk_macros::LightCompressible; +/// use light_sdk::compressible::CompressionInfo; +/// use solana_pubkey::Pubkey; +/// +/// #[derive(Default, Debug, InitSpace, LightCompressible)] +/// #[account] +/// pub struct UserRecord { +/// pub owner: Pubkey, +/// #[max_len(32)] +/// pub name: String, +/// pub score: u64, +/// pub compression_info: Option, +/// } +/// ``` +/// +/// This is equivalent to: +/// ```ignore +/// #[derive(Default, Debug, InitSpace, LightHasherSha, LightDiscriminator, Compressible, CompressiblePack)] +/// #[account] +/// pub struct UserRecord { ... } +/// ``` +/// +/// ## Notes +/// +/// - The `compression_info` field is auto-detected and handled specially (no `#[skip]` needed) +/// - SHA256 hashing serializes the entire struct, so `#[hash]` is not needed +pub fn derive_light_compressible(input: DeriveInput) -> Result { + // Convert DeriveInput to ItemStruct for macros that need it + let item_struct = derive_input_to_item_struct(&input)?; + + // Generate LightHasherSha implementation + let hasher_impl = derive_light_hasher_sha(item_struct.clone())?; + + // Generate LightDiscriminator implementation + let discriminator_impl = discriminator(item_struct)?; + + // Generate Compressible implementation (HasCompressionInfo + CompressAs + Size + CompressedInitSpace) + let compressible_impl = derive_compressible(input.clone())?; + + // Generate CompressiblePack implementation (Pack + Unpack + Packed struct) + let pack_impl = derive_compressible_pack(input)?; + + // Combine all implementations + Ok(quote! { + #hasher_impl + #discriminator_impl + #compressible_impl + #pack_impl + }) +} + +/// Converts a DeriveInput to an ItemStruct. +/// +/// This is needed because some of our existing macros (like LightHasherSha) +/// expect ItemStruct while others (like Compressible) expect DeriveInput. +fn derive_input_to_item_struct(input: &DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + _ => { + return Err(syn::Error::new_spanned( + input, + "LightCompressible can only be derived for structs", + )) + } + }; + + let fields = match &data.fields { + Fields::Named(fields) => Fields::Named(fields.clone()), + Fields::Unnamed(fields) => Fields::Unnamed(fields.clone()), + Fields::Unit => Fields::Unit, + }; + + Ok(ItemStruct { + attrs: input.attrs.clone(), + vis: input.vis.clone(), + struct_token: data.struct_token, + ident: input.ident.clone(), + generics: input.generics.clone(), + fields, + semi_token: data.semi_token, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use syn::parse_quote; + + #[test] + fn test_light_compressible_basic() { + // No #[hash] or #[skip] needed - SHA256 hashes entire struct, compression_info auto-skipped + let input: DeriveInput = parse_quote! { + pub struct UserRecord { + pub owner: Pubkey, + pub name: String, + pub score: u64, + pub compression_info: Option, + } + }; + + let result = derive_light_compressible(input); + assert!(result.is_ok(), "LightCompressible should succeed"); + + let output = result.unwrap().to_string(); + + // Should contain LightHasherSha output + assert!( + output.contains("DataHasher"), + "Should implement DataHasher" + ); + assert!( + output.contains("ToByteArray"), + "Should implement ToByteArray" + ); + + // Should contain LightDiscriminator output + assert!( + output.contains("LightDiscriminator"), + "Should implement LightDiscriminator" + ); + assert!( + output.contains("LIGHT_DISCRIMINATOR"), + "Should have discriminator constant" + ); + + // Should contain Compressible output (HasCompressionInfo, CompressAs, Size) + assert!( + output.contains("HasCompressionInfo"), + "Should implement HasCompressionInfo" + ); + assert!( + output.contains("CompressAs"), + "Should implement CompressAs" + ); + assert!(output.contains("Size"), "Should implement Size"); + + // Should contain CompressiblePack output (Pack, Unpack, Packed struct) + assert!(output.contains("Pack"), "Should implement Pack"); + assert!(output.contains("Unpack"), "Should implement Unpack"); + assert!( + output.contains("PackedUserRecord"), + "Should generate Packed struct" + ); + } + + #[test] + fn test_light_compressible_with_compress_as() { + // compress_as still works - no #[hash] or #[skip] needed + let input: DeriveInput = parse_quote! { + #[compress_as(start_time = 0, score = 0)] + pub struct GameSession { + pub session_id: u64, + pub player: Pubkey, + pub start_time: u64, + pub score: u64, + pub compression_info: Option, + } + }; + + let result = derive_light_compressible(input); + assert!( + result.is_ok(), + "LightCompressible with compress_as should succeed" + ); + + let output = result.unwrap().to_string(); + + // compress_as attribute should be processed + assert!( + output.contains("CompressAs"), + "Should implement CompressAs" + ); + } + + #[test] + fn test_light_compressible_no_pubkey_fields() { + let input: DeriveInput = parse_quote! { + pub struct SimpleRecord { + pub id: u64, + pub value: u32, + pub compression_info: Option, + } + }; + + let result = derive_light_compressible(input); + assert!( + result.is_ok(), + "LightCompressible without Pubkey fields should succeed" + ); + + let output = result.unwrap().to_string(); + + // Should still generate everything + assert!( + output.contains("DataHasher"), + "Should implement DataHasher" + ); + assert!( + output.contains("LightDiscriminator"), + "Should implement LightDiscriminator" + ); + assert!( + output.contains("HasCompressionInfo"), + "Should implement HasCompressionInfo" + ); + + // For structs without Pubkey fields, PackedSimpleRecord should be a type alias + // (implementation detail of CompressiblePack) + } + + #[test] + fn test_light_compressible_enum_fails() { + let input: DeriveInput = parse_quote! { + pub enum NotAStruct { + A, + B, + } + }; + + let result = derive_light_compressible(input); + assert!( + result.is_err(), + "LightCompressible should fail for enums" + ); + } + + #[test] + fn test_light_compressible_missing_compression_info() { + let input: DeriveInput = parse_quote! { + pub struct MissingCompressionInfo { + pub id: u64, + pub value: u32, + } + }; + + let result = derive_light_compressible(input); + // Compressible derive validates compression_info field + assert!( + result.is_err(), + "Should fail without compression_info field" + ); + } +} diff --git a/sdk-libs/macros/src/compressible/mod.rs b/sdk-libs/macros/src/compressible/mod.rs index fb11aaa1b2..c02abdeb69 100644 --- a/sdk-libs/macros/src/compressible/mod.rs +++ b/sdk-libs/macros/src/compressible/mod.rs @@ -1,7 +1,10 @@ //! Compressible account macro generation. +pub mod anchor_seeds; pub mod decompress_context; +pub mod file_scanner; pub mod instructions; +pub mod light_compressible; pub mod pack_unpack; pub mod seed_providers; pub mod traits; diff --git a/sdk-libs/macros/src/compressible/seed_providers.rs b/sdk-libs/macros/src/compressible/seed_providers.rs index 0f668cfa72..22ad520b27 100644 --- a/sdk-libs/macros/src/compressible/seed_providers.rs +++ b/sdk-libs/macros/src/compressible/seed_providers.rs @@ -6,25 +6,231 @@ use syn::{spanned::Spanned, Ident, Result}; use crate::compressible::instructions::{InstructionDataSpec, SeedElement, TokenSeedSpec}; +<<<<<<< HEAD pub fn generate_token_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Result { let variants = token_seeds.iter().enumerate().map(|(index, spec)| { +======= +/// Extract ctx.* field names from seed elements (both token seeds and authority seeds) +fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { + let mut ctx_fields = Vec::new(); + let mut seen = std::collections::HashSet::new(); + + // Helper to extract ctx.* from a SeedElement + fn extract_from_seed(seed: &SeedElement, ctx_fields: &mut Vec, seen: &mut std::collections::HashSet) { + if let SeedElement::Expression(expr) = seed { + extract_ctx_from_expr(expr, ctx_fields, seen); + } + } + + fn extract_ctx_from_expr(expr: &syn::Expr, ctx_fields: &mut Vec, seen: &mut std::collections::HashSet) { + if let syn::Expr::Field(field_expr) = expr { + if let syn::Member::Named(field_name) = &field_expr.member { + // Check for ctx.accounts.field pattern + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + let field_name_str = field_name.to_string(); + // Skip standard fields + if !matches!(field_name_str.as_str(), "fee_payer" | "rent_sponsor" | "config" | "compression_authority") { + if seen.insert(field_name_str) { + ctx_fields.push(field_name.clone()); + } + } + } + } + } + } + } + } + // Check for ctx.field pattern (shorthand) + else if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + let field_name_str = field_name.to_string(); + if !matches!(field_name_str.as_str(), "fee_payer" | "rent_sponsor" | "config" | "compression_authority") { + if seen.insert(field_name_str) { + ctx_fields.push(field_name.clone()); + } + } + } + } + } + } + } + // Recursively check method calls like max_key(&ctx.field.key(), ...) + else if let syn::Expr::Call(call_expr) = expr { + for arg in &call_expr.args { + extract_ctx_from_expr(arg, ctx_fields, seen); + } + } else if let syn::Expr::Reference(ref_expr) = expr { + extract_ctx_from_expr(&ref_expr.expr, ctx_fields, seen); + } else if let syn::Expr::MethodCall(method_call) = expr { + extract_ctx_from_expr(&method_call.receiver, ctx_fields, seen); + } + } + + // Extract from seeds + for seed in &spec.seeds { + extract_from_seed(seed, &mut ctx_fields, &mut seen); + } + + // Extract from authority seeds too + if let Some(auth_seeds) = &spec.authority { + for seed in auth_seeds { + extract_from_seed(seed, &mut ctx_fields, &mut seen); + } + } + + ctx_fields +} + +pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Result { + // Phase 8: Generate struct variants with ctx.* seed fields + + // Unpacked variants (with Pubkeys) + let unpacked_variants = token_seeds.iter().map(|spec| { +>>>>>>> a606eb113 (wip) + 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 } + }); + + if ctx_fields.is_empty() { + quote! { #variant_name, } + } else { + quote! { #variant_name { #(#fields,)* }, } + } + }); + + // Packed variants (with u8 indices) + 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 } + }); + + if ctx_fields.is_empty() { + quote! { #variant_name, } + } else { + quote! { #variant_name { #(#fields,)* }, } + } + }); + + // Pack impl match arms + let pack_arms = token_seeds.iter().map(|spec| { let variant_name = &spec.variant; - let index_u8 = index as u8; - quote! { - #variant_name = #index_u8, + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + if ctx_fields.is_empty() { + quote! { + CTokenAccountVariant::#variant_name => PackedCTokenAccountVariant::#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! { + CTokenAccountVariant::#variant_name { #(#field_bindings,)* } => { + #(#pack_stmts)* + PackedCTokenAccountVariant::#variant_name { #(#idx_fields,)* } + } + } + } + }); + + // Unpack impl match arms + let unpack_arms = 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! { + PackedCTokenAccountVariant::#variant_name => Ok(CTokenAccountVariant::#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)| { + // Dereference idx since match pattern gives us &u8 + 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(); + + quote! { + PackedCTokenAccountVariant::#variant_name { #(#idx_fields,)* } => { + #(#unpack_stmts)* + Ok(CTokenAccountVariant::#variant_name { #(#field_names,)* }) + } + } } }); Ok(quote! { #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] - #[repr(u8)] pub enum CTokenAccountVariant { - #(#variants)* + #(#unpacked_variants)* + } + + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] + pub enum PackedCTokenAccountVariant { + #(#packed_variants)* + } + + impl light_ctoken_sdk::pack::Pack for CTokenAccountVariant { + type Packed = PackedCTokenAccountVariant; + + fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + match self { + #(#pack_arms)* + } + } + } + + impl light_ctoken_sdk::pack::Unpack for PackedCTokenAccountVariant { + type Unpacked = CTokenAccountVariant; + + fn unpack( + &self, + remaining_accounts: &[solana_account_info::AccountInfo], + ) -> std::result::Result { + match self { + #(#unpack_arms)* + } + } + } + + impl light_sdk::compressible::IntoCTokenVariant for CTokenAccountVariant { + fn into_ctoken_variant(self, token_data: light_ctoken_sdk::compat::TokenData) -> RentFreeAccountVariant { + RentFreeAccountVariant::CTokenData(light_ctoken_sdk::compat::CTokenData { + variant: self, + token_data, + }) + } } }) } +<<<<<<< HEAD pub fn generate_token_seed_provider_implementation( +======= +/// Phase 8: Generate CTokenSeedProvider impl that uses self.field instead of ctx.accounts.field +pub fn generate_ctoken_seed_provider_implementation( +>>>>>>> a606eb113 (wip) token_seeds: &[TokenSeedSpec], ) -> Result { let mut get_seeds_match_arms = Vec::new(); @@ -32,149 +238,61 @@ pub fn generate_token_seed_provider_implementation( for spec in token_seeds { let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); - if spec.is_ata { - let get_seeds_arm = quote! { - CTokenAccountVariant::#variant_name => { - Err(anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::AtaDoesNotUseSeedDerivation.into() - ).into()) - } - }; - get_seeds_match_arms.push(get_seeds_arm); - - let authority_arm = quote! { - CTokenAccountVariant::#variant_name => { - Err(anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::AtaDoesNotUseSeedDerivation.into() - ).into()) - } - }; - get_authority_seeds_match_arms.push(authority_arm); - continue; - } - - let mut token_bindings = Vec::new(); - let mut token_seed_refs = Vec::new(); + // Build match pattern with destructuring if there are ctx fields + let pattern = if ctx_fields.is_empty() { + quote! { CTokenAccountVariant::#variant_name } + } else { + let field_names: Vec<_> = ctx_fields.iter().collect(); + quote! { CTokenAccountVariant::#variant_name { #(#field_names,)* } } + }; - for (i, seed) in spec.seeds.iter().enumerate() { + // Build seed refs for get_seeds - use self.field directly for ctx.* seeds + let token_seed_refs: Vec = spec.seeds.iter().map(|seed| { match seed { SeedElement::Literal(lit) => { let value = lit.value(); - token_seed_refs.push(quote! { #value.as_bytes() }); + quote! { #value.as_bytes() } } SeedElement::Expression(expr) => { + // Handle byte string literals + if let syn::Expr::Lit(lit_expr) = &**expr { + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + return quote! { &[#(#bytes),*] }; + } + } + + // Handle uppercase constants if let syn::Expr::Path(path_expr) = &**expr { if let Some(ident) = path_expr.path.get_ident() { let ident_str = ident.to_string(); - if ident_str - .chars() - .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) - { + if ident_str.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) { if ident_str == "LIGHT_CPI_SIGNER" { - token_seed_refs.push(quote! { #ident.cpi_signer.as_ref() }); + return quote! { crate::#ident.cpi_signer.as_ref() }; } else { - token_seed_refs.push(quote! { #ident.as_bytes() }); + return quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }; } - continue; } } } - let mut handled = false; - if let syn::Expr::Field(field_expr) = &**expr { - if let syn::Member::Named(field_name) = &field_expr.member { - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - let binding_name = syn::Ident::new( - &format!("seed_{}", i), - expr.span(), - ); - let field_name_str = field_name.to_string(); - let is_standard_field = matches!( - field_name_str.as_str(), - "fee_payer" - | "rent_sponsor" - | "config" - | "compression_authority" - ); - if is_standard_field { - token_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name.key(); - }); - } else { - token_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name - .as_ref() - .ok_or_else(|| -> anchor_lang::error::Error { - anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into() - })? - .key(); - }); - } - token_seed_refs - .push(quote! { #binding_name.as_ref() }); - handled = true; - } - } - } - } - } - } else if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - let binding_name = - syn::Ident::new(&format!("seed_{}", i), expr.span()); - let field_name_str = field_name.to_string(); - let is_standard_field = matches!( - field_name_str.as_str(), - "fee_payer" - | "rent_sponsor" - | "config" - | "compression_authority" - ); - if is_standard_field { - token_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name.key(); - }); - } else { - token_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name - .as_ref() - .ok_or_else(|| -> anchor_lang::error::Error { - anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into() - })? - .key(); - }); - } - token_seed_refs.push(quote! { #binding_name.as_ref() }); - handled = true; - } - } - } - } + // Handle ctx.accounts.field or ctx.field - use the destructured field directly + if let Some(field_name) = extract_ctx_field_name(expr) { + return quote! { #field_name.as_ref() }; } - if !handled { - token_seed_refs.push(quote! { (#expr).as_ref() }); - } + // Fallback + quote! { (#expr).as_ref() } } } - } + }).collect(); let get_seeds_arm = quote! { - CTokenAccountVariant::#variant_name => { - #(#token_bindings)* + #pattern => { let seeds: &[&[u8]] = &[#(#token_seed_refs),*]; - let (token_account_pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &crate::ID); + let (token_account_pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id); let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); seeds_vec.push(vec![bump]); @@ -183,140 +301,52 @@ pub fn generate_token_seed_provider_implementation( }; get_seeds_match_arms.push(get_seeds_arm); + // Build authority seeds if let Some(authority_seeds) = &spec.authority { - let mut auth_bindings: Vec = Vec::new(); - let mut auth_seed_refs = Vec::new(); - - for (i, authority_seed) in authority_seeds.iter().enumerate() { - match authority_seed { + let auth_seed_refs: Vec = authority_seeds.iter().map(|seed| { + match seed { SeedElement::Literal(lit) => { let value = lit.value(); - auth_seed_refs.push(quote! { #value.as_bytes() }); + quote! { #value.as_bytes() } } SeedElement::Expression(expr) => { - let mut handled = false; - match &**expr { - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member - { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = - path.path.segments.first() - { - if segment.ident == "ctx" { - let binding_name = syn::Ident::new( - &format!("authority_seed_{}", i), - expr.span(), - ); - let field_name_str = - field_name.to_string(); - let is_standard_field = matches!( - field_name_str.as_str(), - "fee_payer" - | "rent_sponsor" - | "config" - | "compression_authority" - ); - if is_standard_field { - auth_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name.key(); - }); - } else { - auth_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name - .as_ref() - .ok_or_else(|| -> anchor_lang::error::Error { - anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into() - })? - .key(); - }); - } - auth_seed_refs.push( - quote! { #binding_name.as_ref() }, - ); - handled = true; - } - } - } - } - } - } else if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - let binding_name = syn::Ident::new( - &format!("authority_seed_{}", i), - expr.span(), - ); - let field_name_str = field_name.to_string(); - let is_standard_field = matches!( - field_name_str.as_str(), - "fee_payer" - | "rent_sponsor" - | "config" - | "compression_authority" - ); - if is_standard_field { - auth_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name.key(); - }); - } else { - auth_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name - .as_ref() - .ok_or_else(|| -> anchor_lang::error::Error { - anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into() - })? - .key(); - }); - } - auth_seed_refs - .push(quote! { #binding_name.as_ref() }); - handled = true; - } - } - } - } + // Handle byte string literals + if let syn::Expr::Lit(lit_expr) = &**expr { + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + return quote! { &[#(#bytes),*] }; } - syn::Expr::MethodCall(_mc) => { - auth_seed_refs.push(quote! { (#expr).as_ref() }); - handled = true; - } - syn::Expr::Path(path_expr) => { - if let Some(ident) = path_expr.path.get_ident() { - let ident_str = ident.to_string(); - if ident_str.chars().all(|c| c.is_uppercase() || c == '_') { - if ident_str == "LIGHT_CPI_SIGNER" { - auth_seed_refs - .push(quote! { #ident.cpi_signer.as_ref() }); - } else { - auth_seed_refs.push(quote! { #ident.as_bytes() }); - } - handled = true; + } + + // Handle uppercase constants + if let syn::Expr::Path(path_expr) = &**expr { + if let Some(ident) = path_expr.path.get_ident() { + let ident_str = ident.to_string(); + if ident_str.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) { + if ident_str == "LIGHT_CPI_SIGNER" { + return quote! { crate::#ident.cpi_signer.as_ref() }; + } else { + return quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }; } } } - _ => {} } - if !handled { - auth_seed_refs.push(quote! { (#expr).as_ref() }); + // Handle ctx.accounts.field or ctx.field - use the destructured field directly + if let Some(field_name) = extract_ctx_field_name(expr) { + return quote! { #field_name.as_ref() }; } + + // Fallback + quote! { (#expr).as_ref() } } } - } + }).collect(); let authority_arm = quote! { - CTokenAccountVariant::#variant_name => { - #(#auth_bindings)* + #pattern => { let seeds: &[&[u8]] = &[#(#auth_seed_refs),*]; - let (authority_pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &crate::ID); + let (authority_pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id); let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); seeds_vec.push(vec![bump]); @@ -326,45 +356,71 @@ pub fn generate_token_seed_provider_implementation( get_authority_seeds_match_arms.push(authority_arm); } else { let authority_arm = quote! { - CTokenAccountVariant::#variant_name => { - Err(anchor_lang::prelude::ProgramError::Custom( + #pattern => { + Err(solana_program_error::ProgramError::Custom( CompressibleInstructionError::MissingSeedAccount.into() - ).into()) + )) } }; get_authority_seeds_match_arms.push(authority_arm); } } + // Phase 8: New trait signature - no ctx/accounts parameter needed Ok(quote! { - impl ctoken_seed_system::CTokenSeedProvider for CTokenAccountVariant { - fn get_seeds<'a, 'info>( + impl light_sdk::compressible::CTokenSeedProvider for CTokenAccountVariant { + fn get_seeds( &self, - ctx: &ctoken_seed_system::CTokenSeedContext<'a, 'info>, - ) -> Result<(Vec>, solana_pubkey::Pubkey)> { + program_id: &solana_pubkey::Pubkey, + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { match self { #(#get_seeds_match_arms)* - _ => Err(anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into()) } } - fn get_authority_seeds<'a, 'info>( + fn get_authority_seeds( &self, - ctx: &ctoken_seed_system::CTokenSeedContext<'a, 'info>, - ) -> Result<(Vec>, solana_pubkey::Pubkey)> { + program_id: &solana_pubkey::Pubkey, + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { match self { #(#get_authority_seeds_match_arms)* - _ => Err(anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into()) } } } }) } +/// Extract the field name from a ctx.field or ctx.accounts.field expression +fn extract_ctx_field_name(expr: &syn::Expr) -> Option { + if let syn::Expr::Field(field_expr) = expr { + if let syn::Member::Named(field_name) = &field_expr.member { + // Check for ctx.accounts.field pattern + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + return Some(field_name.clone()); + } + } + } + } + } + } + // Check for ctx.field pattern (shorthand) + else if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + return Some(field_name.clone()); + } + } + } + } + } + None +} + #[inline(never)] pub fn generate_client_seed_functions( _account_types: &[Ident], @@ -404,10 +460,6 @@ pub fn generate_client_seed_functions( for spec in token_seed_specs { let variant_name = &spec.variant; - if spec.is_ata { - continue; - } - let function_name = format_ident!("get_{}_seeds", variant_name.to_string().to_lowercase()); @@ -439,7 +491,6 @@ pub fn generate_client_seed_functions( variant: spec.variant.clone(), _eq: spec._eq, is_token: spec.is_token, - is_ata: spec.is_ata, seeds: syn::punctuated::Punctuated::new(), authority: None, }; @@ -639,6 +690,13 @@ fn analyze_seed_spec_for_client( } } } + syn::Expr::Lit(lit_expr) => { + // Handle byte string literals: b"seed" -> use directly + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + expressions.push(quote! { &[#(#bytes),*] }); + } + } syn::Expr::Path(path_expr) => { if let Some(ident) = path_expr.path.get_ident() { let ident_str = ident.to_string(); @@ -647,9 +705,10 @@ fn analyze_seed_spec_for_client( .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) { if ident_str == "LIGHT_CPI_SIGNER" { - expressions.push(quote! { #ident.cpi_signer.as_ref() }); + expressions.push(quote! { crate::#ident.cpi_signer.as_ref() }); } else { - expressions.push(quote! { #ident.as_bytes() }); + // Use crate:: prefix and explicit type annotation + expressions.push(quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }); } } else { parameters.push(quote! { #ident: &solana_pubkey::Pubkey }); diff --git a/sdk-libs/macros/src/compressible/traits.rs b/sdk-libs/macros/src/compressible/traits.rs index a59b97af53..4118b5b17e 100644 --- a/sdk-libs/macros/src/compressible/traits.rs +++ b/sdk-libs/macros/src/compressible/traits.rs @@ -83,7 +83,8 @@ fn generate_has_compression_info_impl(struct_name: &Ident) -> TokenStream { } } -/// Generates field assignments for CompressAs trait, handling overrides and copy types +/// Generates field assignments for CompressAs trait, handling overrides and copy types. +/// Auto-skips `compression_info` field and fields marked with `#[skip]`. fn generate_compress_as_field_assignments( fields: &Punctuated, compress_as_fields: &Option, @@ -94,6 +95,12 @@ fn generate_compress_as_field_assignments( let field_name = field.ident.as_ref().unwrap(); let field_type = &field.ty; + // Auto-skip compression_info field (handled separately in CompressAs impl) + if field_name == "compression_info" { + continue; + } + + // Also skip fields explicitly marked with #[skip] if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) { continue; } @@ -147,13 +154,20 @@ fn generate_compress_as_impl( } } -/// Generates size calculation fields for the Size trait +/// Generates size calculation fields for the Size trait. +/// Auto-skips `compression_info` field and fields marked with `#[skip]`. fn generate_size_fields(fields: &Punctuated) -> Vec { let mut size_fields = Vec::new(); for field in fields.iter() { let field_name = field.ident.as_ref().unwrap(); + // Auto-skip compression_info field (handled separately in Size impl) + if field_name == "compression_info" { + continue; + } + + // Also skip fields explicitly marked with #[skip] if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) { continue; } diff --git a/sdk-libs/macros/src/compressible/variant_enum.rs b/sdk-libs/macros/src/compressible/variant_enum.rs index c220a8c15a..177558b54c 100644 --- a/sdk-libs/macros/src/compressible/variant_enum.rs +++ b/sdk-libs/macros/src/compressible/variant_enum.rs @@ -1,70 +1,104 @@ use proc_macro2::TokenStream; -use quote::quote; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - Ident, Result, Token, -}; - -struct AccountTypeList { - types: Punctuated, +use quote::{format_ident, quote}; +use syn::{Ident, Result}; + +/// Info about ctx.* seeds for a PDA type +#[derive(Clone, Debug)] +pub struct PdaCtxSeedInfo { + pub type_name: Ident, + /// Field names from ctx.accounts.XXX references in seeds + pub ctx_seed_fields: Vec, } -impl Parse for AccountTypeList { - fn parse(input: ParseStream) -> Result { - Ok(AccountTypeList { - types: Punctuated::parse_terminated(input)?, - }) +impl PdaCtxSeedInfo { + pub fn new(type_name: Ident, ctx_seed_fields: Vec) -> Self { + Self { + type_name, + ctx_seed_fields, + } } } -pub fn compressed_account_variant(input: TokenStream) -> Result { - let type_list = syn::parse2::(input)?; - let account_types: Vec<&Ident> = type_list.types.iter().collect(); - +/// Enhanced function that generates variants with ctx.* seed fields +pub fn compressed_account_variant_with_ctx_seeds( + account_types: &[&Ident], + pda_ctx_seeds: &[PdaCtxSeedInfo], +) -> Result { if account_types.is_empty() { - return Err(syn::Error::new_spanned( - &type_list.types, + return Err(syn::Error::new( + proc_macro2::Span::call_site(), "At least one account type must be specified", )); } + // Build a map from type name to ctx seed fields + let ctx_seeds_map: std::collections::HashMap = pda_ctx_seeds + .iter() + .map(|info| (info.type_name.to_string(), info.ctx_seed_fields.as_slice())) + .collect(); + + // Phase 2: Generate struct variants with ctx.* seed fields let account_variants = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); + let ctx_fields = ctx_seeds_map.get(&name.to_string()).copied().unwrap_or(&[]); + + // Unpacked variant: Pubkey fields for ctx.* seeds + // 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 } + }); + + // Packed variant: u8 index fields for ctx.* seeds + let packed_ctx_fields = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { #idx_field: u8 } + }); + quote! { - #name(#name), - #packed_name(#packed_name), + #name { data: #name, #(#unpacked_ctx_fields,)* }, + #packed_name { data: #packed_name, #(#packed_ctx_fields,)* }, } }); + // Phase 8: PackedCTokenData uses PackedCTokenAccountVariant (with idx fields) + // CTokenData uses CTokenAccountVariant (with Pubkey fields) let enum_def = quote! { #[derive(Clone, Debug, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] - pub enum CompressedAccountVariant { + pub enum RentFreeAccountVariant { #(#account_variants)* +<<<<<<< HEAD PackedCTokenData(light_token_sdk::compat::PackedCTokenData), CTokenData(light_token_sdk::compat::CTokenData), +======= + PackedCTokenData(light_ctoken_sdk::compat::PackedCTokenData), + CTokenData(light_ctoken_sdk::compat::CTokenData), +>>>>>>> a606eb113 (wip) } }; let first_type = account_types[0]; + let first_ctx_fields = ctx_seeds_map.get(&first_type.to_string()).copied().unwrap_or(&[]); + let first_default_ctx_fields = first_ctx_fields.iter().map(|field| { + quote! { #field: Pubkey::default() } + }); let default_impl = quote! { - impl Default for CompressedAccountVariant { + impl Default for RentFreeAccountVariant { fn default() -> Self { - Self::#first_type(#first_type::default()) + Self::#first_type { data: #first_type::default(), #(#first_default_ctx_fields,)* } } } }; let hash_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_hasher::DataHasher>::hash::(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_hasher::DataHasher>::hash::(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let data_hasher_impl = quote! { - impl light_hasher::DataHasher for CompressedAccountVariant { + impl light_hasher::DataHasher for RentFreeAccountVariant { fn hash(&self) -> std::result::Result<[u8; 32], light_hasher::HasherError> { match self { #(#hash_match_arms)* @@ -76,46 +110,46 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { }; let light_discriminator_impl = quote! { - impl light_sdk::LightDiscriminator for CompressedAccountVariant { + 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_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let compression_info_mut_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let compression_info_mut_opt_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut_opt(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut_opt(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let set_compression_info_none_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::set_compression_info_none(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::set_compression_info_none(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let has_compression_info_impl = quote! { - impl light_sdk::compressible::HasCompressionInfo for CompressedAccountVariant { + impl light_sdk::compressible::HasCompressionInfo for RentFreeAccountVariant { fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { match self { #(#compression_info_match_arms)* @@ -151,15 +185,15 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { }; let size_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_sdk::account::Size>::size(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::account::Size>::size(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let size_impl = quote! { - impl light_sdk::account::Size for CompressedAccountVariant { + impl light_sdk::account::Size for RentFreeAccountVariant { fn size(&self) -> usize { match self { #(#size_match_arms)* @@ -170,16 +204,44 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { } }; - let pack_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); - quote! { - CompressedAccountVariant::#packed_name(_) => unreachable!(), - CompressedAccountVariant::#name(data) => CompressedAccountVariant::#packed_name(<#name as light_sdk::compressible::Pack>::pack(data, remaining_accounts)), + // Phase 2: Pack/Unpack with ctx seed fields + let pack_match_arms: Vec<_> = account_types.iter().map(|name| { + let packed_name = format_ident!("Packed{}", name); + let ctx_fields = ctx_seeds_map.get(&name.to_string()).copied().unwrap_or(&[]); + + if ctx_fields.is_empty() { + // No ctx seeds - simple pack + quote! { + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => RentFreeAccountVariant::#packed_name { + data: <#name as light_sdk::compressible::Pack>::pack(data, remaining_accounts), + }, + } + } else { + // Has ctx seeds - pack data and ctx seed pubkeys + let 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(); + + quote! { + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), + RentFreeAccountVariant::#name { data, #(#field_names,)* .. } => { + #(#pack_ctx_seeds)* + RentFreeAccountVariant::#packed_name { + data: <#name as light_sdk::compressible::Pack>::pack(data, remaining_accounts), + #(#idx_field_names,)* + } + }, + } } - }); + }).collect(); let pack_impl = quote! { - impl light_sdk::compressible::Pack for CompressedAccountVariant { + impl light_sdk::compressible::Pack for RentFreeAccountVariant { type Packed = Self; fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { @@ -187,23 +249,59 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { #(#pack_match_arms)* Self::PackedCTokenData(_) => unreachable!(), Self::CTokenData(data) => { +<<<<<<< HEAD Self::PackedCTokenData(light_token_sdk::pack::Pack::pack(data, remaining_accounts)) +======= + // Use ctoken-sdk's Pack trait for CTokenData + Self::PackedCTokenData(light_ctoken_sdk::pack::Pack::pack(data, remaining_accounts)) +>>>>>>> a606eb113 (wip) } } } } }; - let unpack_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); - quote! { - CompressedAccountVariant::#packed_name(data) => Ok(CompressedAccountVariant::#name(<#packed_name as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?)), - CompressedAccountVariant::#name(_) => unreachable!(), + let unpack_match_arms: Vec<_> = account_types.iter().map(|name| { + let packed_name = format_ident!("Packed{}", name); + let ctx_fields = ctx_seeds_map.get(&name.to_string()).copied().unwrap_or(&[]); + + if ctx_fields.is_empty() { + // No ctx seeds - simple unpack + quote! { + RentFreeAccountVariant::#packed_name { data, .. } => Ok(RentFreeAccountVariant::#name { + data: <#packed_name as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, + }), + RentFreeAccountVariant::#name { .. } => unreachable!(), + } + } else { + // Has ctx seeds - unpack data and resolve ctx seed pubkeys from indices + let idx_field_names: Vec<_> = ctx_fields.iter().map(|f| format_ident!("{}_idx", f)).collect(); + let 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(); + + quote! { + RentFreeAccountVariant::#packed_name { data, #(#idx_field_names,)* .. } => { + #(#unpack_ctx_seeds)* + Ok(RentFreeAccountVariant::#name { + data: <#packed_name as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, + #(#field_names,)* + }) + }, + RentFreeAccountVariant::#name { .. } => unreachable!(), + } } - }); + }).collect(); let unpack_impl = quote! { - impl light_sdk::compressible::Unpack for CompressedAccountVariant { + impl light_sdk::compressible::Unpack for RentFreeAccountVariant { type Unpacked = Self; fn unpack( @@ -219,15 +317,11 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { } }; - let compressed_account_data_struct = quote! { + let rentfree_account_data_struct = quote! { #[derive(Clone, Debug, anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)] - pub struct CompressedAccountData { + pub struct RentFreeAccountData { pub meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - pub data: CompressedAccountVariant, - // /// Indices into remaining_accounts for seed account references (starting from seed_accounts_offset) - // pub seed_indices: Vec, - // /// Indices into remaining_accounts for authority seed references (for CTokens only) - // pub authority_indices: Vec, + pub data: RentFreeAccountVariant, } }; @@ -240,7 +334,7 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { #size_impl #pack_impl #unpack_impl - #compressed_account_data_struct + #rentfree_account_data_struct }; Ok(expanded) diff --git a/sdk-libs/macros/src/finalize/codegen.rs b/sdk-libs/macros/src/finalize/codegen.rs new file mode 100644 index 0000000000..df40420c12 --- /dev/null +++ b/sdk-libs/macros/src/finalize/codegen.rs @@ -0,0 +1,692 @@ +//! Code generation for LightFinalize and LightPreInit trait implementations. +//! +//! Design for mints: +//! - At mint init, we CREATE + DECOMPRESS atomically +//! - After init, the CMint should always be in decompressed/"hot" state +//! +//! Flow for PDAs + mints: +//! 1. Pre-init: ALL compression logic executes here +//! a. Write PDAs to CPI context +//! b. Invoke mint_action with decompress + CPI context +//! c. CMint is now "hot" and usable +//! 2. Instruction body: Can use hot CMint (mintTo, transfers, etc.) +//! 3. Finalize: No-op (all work done in pre_init) + +use super::parse::{ParsedCompressibleStruct, RentFreeField}; +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +/// Generate both trait implementations +pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream { + let struct_name = &parsed.struct_name; + let (impl_generics, ty_generics, where_clause) = parsed.generics.split_for_impl(); + + // Get the params type from instruction args (first arg) + let params_type = parsed + .instruction_args + .as_ref() + .and_then(|args| args.first()) + .map(|arg| &arg.ty); + + let params_type = match params_type { + Some(ty) => ty, + None => { + // No instruction args - generate no-op impls + return quote! { + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightPreInit<'info, ()> for #struct_name #ty_generics #where_clause { + fn light_pre_init( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + _params: &(), + ) -> std::result::Result { + Ok(false) + } + } + + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightFinalize<'info, ()> for #struct_name #ty_generics #where_clause { + fn light_finalize( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + _params: &(), + _has_pre_init: bool, + ) -> std::result::Result<(), light_sdk::error::LightSdkError> { + Ok(()) + } + } + }; + } + }; + + let params_ident = parsed + .instruction_args + .as_ref() + .and_then(|args| args.first()) + .map(|arg| &arg.name) + .expect("params ident must exist if type exists"); + + let has_pdas = !parsed.rentfree_fields.is_empty(); + let has_mints = !parsed.light_mint_fields.is_empty(); + + // Get fee payer field + let fee_payer = parsed + .fee_payer_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { fee_payer }); + + let compression_config = parsed + .compression_config_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { compression_config }); + + // CToken accounts for decompress + let ctoken_config = parsed + .ctoken_config_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { ctoken_compressible_config }); + + let ctoken_rent_sponsor = parsed + .ctoken_rent_sponsor_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { ctoken_rent_sponsor }); + + let ctoken_program = parsed + .ctoken_program_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { ctoken_program }); + + let ctoken_cpi_authority = parsed + .ctoken_cpi_authority_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { ctoken_cpi_authority }); + + // Generate LightPreInit impl based on what we have + // ALL compression logic runs in pre_init so instruction body can use hot state + let pre_init_body = if has_pdas && has_mints { + // PDAs + mints: Write PDAs to CPI context, then invoke mint_action with decompress + generate_pre_init_pdas_and_mints( + parsed, + params_ident, + &fee_payer, + &compression_config, + &ctoken_config, + &ctoken_rent_sponsor, + &ctoken_program, + &ctoken_cpi_authority, + ) + } else if has_mints { + // Mints only: Invoke mint_action with decompress (no CPI context) + generate_pre_init_mints_only( + parsed, + params_ident, + &fee_payer, + &ctoken_config, + &ctoken_rent_sponsor, + &ctoken_program, + &ctoken_cpi_authority, + ) + } else if has_pdas { + // PDAs only: Direct invoke (no CPI context needed) + generate_pre_init_pdas_only(parsed, params_ident, &fee_payer, &compression_config) + } else { + quote! { Ok(false) } + }; + + // LightFinalize: No-op (all work done in pre_init) + let finalize_body = 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( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + #params_ident: &#params_type, + ) -> std::result::Result { + use anchor_lang::ToAccountInfo; + #pre_init_body + } + } + + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightFinalize<'info, #params_type> for #struct_name #ty_generics #where_clause { + fn light_finalize( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + #params_ident: &#params_type, + _has_pre_init: bool, + ) -> std::result::Result<(), light_sdk::error::LightSdkError> { + use anchor_lang::ToAccountInfo; + #finalize_body + } + } + } +} + +/// Generate LightPreInit body for PDAs + mints: +/// 1. Write PDAs to CPI context +/// 2. Invoke mint_action with decompress + CPI context +/// After this, CMint is "hot" and usable in instruction body +#[allow(clippy::too_many_arguments)] +fn generate_pre_init_pdas_and_mints( + parsed: &ParsedCompressibleStruct, + params_ident: &syn::Ident, + fee_payer: &TokenStream, + compression_config: &TokenStream, + ctoken_config: &TokenStream, + ctoken_rent_sponsor: &TokenStream, + ctoken_program: &TokenStream, + ctoken_cpi_authority: &TokenStream, +) -> TokenStream { + let (compress_blocks, new_addr_idents) = + generate_pda_compress_blocks(&parsed.rentfree_fields, params_ident); + let rentfree_count = parsed.rentfree_fields.len() as u8; + let pda_count = parsed.rentfree_fields.len(); + + // Get the first PDA's output tree index (for the state tree output queue) + let first_pda_output_tree = &parsed.rentfree_fields[0].output_tree; + + // Get the first mint (we only support one mint currently) + let mint = &parsed.light_mint_fields[0]; + let mint_field_ident = &mint.field_ident; + let mint_signer = &mint.mint_signer; + let authority = &mint.authority; + let decimals = &mint.decimals; + let address_tree_info = &mint.address_tree_info; + + // Use explicit signer_seeds if provided, otherwise empty + let signer_seeds_tokens = if let Some(seeds) = &mint.signer_seeds { + quote! { #seeds } + } else { + quote! { &[] as &[&[u8]] } + }; + + // Build freeze_authority expression + let freeze_authority_tokens = if let Some(freeze_auth) = &mint.freeze_authority { + quote! { Some(*self.#freeze_auth.to_account_info().key) } + } else { + quote! { None } + }; + + // rent_payment defaults to 2 epochs (u8) + let rent_payment_tokens = if let Some(rent) = &mint.rent_payment { + quote! { #rent } + } else { + quote! { 2u8 } + }; + + // write_top_up defaults to 0 (u32) + let write_top_up_tokens = if let Some(top_up) = &mint.write_top_up { + quote! { #top_up } + } else { + quote! { 0u32 } + }; + + // assigned_account_index for mint is after PDAs + let mint_assigned_index = pda_count as u8; + + quote! { + // Build CPI accounts WITH CPI context for batching + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new_with_config( + &self.#fee_payer, + _remaining, + light_sdk_types::cpi_accounts::CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), + ); + + // Load compression config + let compression_config_data = light_sdk::compressible::CompressibleConfig::load_checked( + &self.#compression_config, + &crate::ID + )?; + + // Collect compressed infos for all rentfree PDA accounts + let mut all_compressed_infos = Vec::with_capacity(#rentfree_count as usize); + #(#compress_blocks)* + + // Step 1: Write PDAs to CPI context + let cpi_context_account = cpi_accounts.cpi_context()?; + let cpi_context_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority()?, + cpi_context: cpi_context_account, + cpi_signer: crate::LIGHT_CPI_SIGNER, + }; + + use light_sdk::cpi::{InvokeLightSystemProgram, LightCpiInstruction}; + light_sdk::cpi::v2::LightSystemProgramCpi::new_cpi( + crate::LIGHT_CPI_SIGNER, + #params_ident.create_accounts_proof.proof.clone() + ) + .with_new_addresses(&[#(#new_addr_idents),*]) + .with_account_infos(&all_compressed_infos) + .write_to_cpi_context_first() + .invoke_write_to_cpi_context_first(cpi_context_accounts)?; + + // Step 2: Build and invoke mint_action with decompress + CPI context + { + let __tree_info = &#address_tree_info; + let address_tree = cpi_accounts.get_tree_account_info(__tree_info.address_merkle_tree_pubkey_index as usize)?; + // Output queue is the state tree queue (same as the PDAs' output tree) + let __output_tree_index = #first_pda_output_tree; + let output_queue = cpi_accounts.get_tree_account_info(__output_tree_index as usize)?; + let __tree_pubkey: solana_pubkey::Pubkey = light_sdk::light_account_checks::AccountInfoTrait::pubkey(address_tree); + + let mint_signer_key = self.#mint_signer.to_account_info().key; + let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address( + mint_signer_key, + &__tree_pubkey, + ); + let (mint_pda, cmint_bump) = light_ctoken_sdk::ctoken::find_cmint_address(mint_signer_key); + + let __proof: light_ctoken_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() + .expect("proof is required for mint creation"); + + let __freeze_authority: Option = #freeze_authority_tokens; + + // Build compressed mint instruction data + let compressed_mint_data = light_ctoken_interface::instructions::mint_action::CompressedMintInstructionData { + supply: 0, + decimals: #decimals, + metadata: light_ctoken_interface::state::CompressedMintMetadata { + version: 3, + mint: mint_pda.to_bytes().into(), + cmint_decompressed: false, + compressed_address: compression_address, + }, + mint_authority: Some((*self.#authority.to_account_info().key).to_bytes().into()), + freeze_authority: __freeze_authority.map(|a| a.to_bytes().into()), + extensions: None, + }; + + // Build mint action instruction data with decompress + let mut instruction_data = light_ctoken_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( + __tree_info.root_index, + __proof, + compressed_mint_data, + ) + .with_decompress_mint(light_ctoken_interface::instructions::mint_action::DecompressMintAction { + cmint_bump, + rent_payment: #rent_payment_tokens, + write_top_up: #write_top_up_tokens, + }) + .with_cpi_context(light_ctoken_interface::instructions::mint_action::CpiContext { + address_tree_pubkey: __tree_pubkey.to_bytes(), + set_context: false, + first_set_context: false, // PDAs already wrote to context + // in_tree_index is 1-indexed and points to the state queue (for CPI context validation) + // The Light System Program does `in_tree_index - 1` and uses queue's associated_merkle_tree + in_tree_index: __output_tree_index + 1, // +1 because 1-indexed + in_queue_index: __output_tree_index, + out_queue_index: __output_tree_index, // Output state queue + token_out_queue_index: 0, + assigned_account_index: #mint_assigned_index, + read_only_address_trees: [0; 4], + }); + + // Build account metas with compressible CMint + let mut meta_config = light_ctoken_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( + *self.#fee_payer.to_account_info().key, + *self.#authority.to_account_info().key, + *mint_signer_key, + __tree_pubkey, + *output_queue.key, + ) + .with_compressible_cmint( + mint_pda, + *self.#ctoken_config.to_account_info().key, + *self.#ctoken_rent_sponsor.to_account_info().key, + ); + + meta_config.cpi_context = Some(*cpi_accounts.cpi_context()?.key); + + let account_metas = meta_config.to_account_metas(); + + use light_compressed_account::instruction_data::traits::LightInstructionData; + let ix_data = instruction_data.data() + .map_err(|e| light_sdk::error::LightSdkError::Borsh)?; + + let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { + program_id: solana_pubkey::Pubkey::new_from_array(light_ctoken_interface::CTOKEN_PROGRAM_ID), + accounts: account_metas, + data: ix_data, + }; + + // Build account infos and invoke + // Include all accounts needed for mint_action with decompress + let mut account_infos = cpi_accounts.to_account_infos(); + // Add ctoken-specific accounts that aren't in the Light System CPI accounts + account_infos.push(self.#ctoken_program.to_account_info()); + account_infos.push(self.#ctoken_cpi_authority.to_account_info()); + account_infos.push(self.#mint_field_ident.to_account_info()); + account_infos.push(self.#ctoken_config.to_account_info()); + account_infos.push(self.#ctoken_rent_sponsor.to_account_info()); + account_infos.push(self.#authority.to_account_info()); + account_infos.push(self.#mint_signer.to_account_info()); + account_infos.push(self.#fee_payer.to_account_info()); + + let signer_seeds: &[&[u8]] = #signer_seeds_tokens; + if signer_seeds.is_empty() { + anchor_lang::solana_program::program::invoke(&mint_action_ix, &account_infos)?; + } else { + anchor_lang::solana_program::program::invoke_signed(&mint_action_ix, &account_infos, &[signer_seeds])?; + } + } + + Ok(true) + } +} + +/// Generate LightPreInit body for mints-only (no PDAs): +/// Invoke mint_action with decompress directly +/// After this, CMint is "hot" and usable in instruction body +#[allow(clippy::too_many_arguments)] +fn generate_pre_init_mints_only( + parsed: &ParsedCompressibleStruct, + params_ident: &syn::Ident, + fee_payer: &TokenStream, + ctoken_config: &TokenStream, + ctoken_rent_sponsor: &TokenStream, + ctoken_program: &TokenStream, + ctoken_cpi_authority: &TokenStream, +) -> TokenStream { + // Get the first mint (we only support one mint currently) + let mint = &parsed.light_mint_fields[0]; + let mint_field_ident = &mint.field_ident; + let mint_signer = &mint.mint_signer; + let authority = &mint.authority; + let decimals = &mint.decimals; + let address_tree_info = &mint.address_tree_info; + + // Use explicit signer_seeds if provided, otherwise empty + let signer_seeds_tokens = if let Some(seeds) = &mint.signer_seeds { + quote! { #seeds } + } else { + quote! { &[] as &[&[u8]] } + }; + + // Build freeze_authority expression + let freeze_authority_tokens = if let Some(freeze_auth) = &mint.freeze_authority { + quote! { Some(*self.#freeze_auth.to_account_info().key) } + } else { + quote! { None } + }; + + // rent_payment defaults to 2 epochs (u8) + let rent_payment_tokens = if let Some(rent) = &mint.rent_payment { + quote! { #rent } + } else { + quote! { 2u8 } + }; + + // write_top_up defaults to 0 (u32) + let write_top_up_tokens = if let Some(top_up) = &mint.write_top_up { + quote! { #top_up } + } else { + quote! { 0u32 } + }; + + quote! { + // Build CPI accounts (no CPI context needed for mints-only) + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( + &self.#fee_payer, + _remaining, + crate::LIGHT_CPI_SIGNER, + ); + + // Build and invoke mint_action with decompress + { + let __tree_info = &#address_tree_info; + let address_tree = cpi_accounts.get_tree_account_info(__tree_info.address_merkle_tree_pubkey_index as usize)?; + let output_queue = cpi_accounts.get_tree_account_info(__tree_info.address_queue_pubkey_index as usize)?; + let __tree_pubkey: solana_pubkey::Pubkey = light_sdk::light_account_checks::AccountInfoTrait::pubkey(address_tree); + + let mint_signer_key = self.#mint_signer.to_account_info().key; + let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address( + mint_signer_key, + &__tree_pubkey, + ); + let (mint_pda, cmint_bump) = light_ctoken_sdk::ctoken::find_cmint_address(mint_signer_key); + + let __proof: light_ctoken_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() + .expect("proof is required for mint creation"); + + let __freeze_authority: Option = #freeze_authority_tokens; + + // Build compressed mint instruction data + let compressed_mint_data = light_ctoken_interface::instructions::mint_action::CompressedMintInstructionData { + supply: 0, + decimals: #decimals, + metadata: light_ctoken_interface::state::CompressedMintMetadata { + version: 3, + mint: mint_pda.to_bytes().into(), + cmint_decompressed: false, + compressed_address: compression_address, + }, + mint_authority: Some((*self.#authority.to_account_info().key).to_bytes().into()), + freeze_authority: __freeze_authority.map(|a| a.to_bytes().into()), + extensions: None, + }; + + // Build mint action instruction data with decompress (no CPI context) + let instruction_data = light_ctoken_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( + __tree_info.root_index, + __proof, + compressed_mint_data, + ) + .with_decompress_mint(light_ctoken_interface::instructions::mint_action::DecompressMintAction { + cmint_bump, + rent_payment: #rent_payment_tokens, + write_top_up: #write_top_up_tokens, + }); + + // Build account metas with compressible CMint + let meta_config = light_ctoken_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( + *self.#fee_payer.to_account_info().key, + *self.#authority.to_account_info().key, + *mint_signer_key, + __tree_pubkey, + *output_queue.key, + ) + .with_compressible_cmint( + mint_pda, + *self.#ctoken_config.to_account_info().key, + *self.#ctoken_rent_sponsor.to_account_info().key, + ); + + let account_metas = meta_config.to_account_metas(); + + use light_compressed_account::instruction_data::traits::LightInstructionData; + let ix_data = instruction_data.data() + .map_err(|e| light_sdk::error::LightSdkError::Borsh)?; + + let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { + program_id: solana_pubkey::Pubkey::new_from_array(light_ctoken_interface::CTOKEN_PROGRAM_ID), + accounts: account_metas, + data: ix_data, + }; + + // Build account infos and invoke + let mut account_infos = cpi_accounts.to_account_infos(); + // Add ctoken-specific accounts + account_infos.push(self.#ctoken_program.to_account_info()); + account_infos.push(self.#ctoken_cpi_authority.to_account_info()); + account_infos.push(self.#mint_field_ident.to_account_info()); + account_infos.push(self.#ctoken_config.to_account_info()); + account_infos.push(self.#ctoken_rent_sponsor.to_account_info()); + account_infos.push(self.#authority.to_account_info()); + account_infos.push(self.#mint_signer.to_account_info()); + account_infos.push(self.#fee_payer.to_account_info()); + + let signer_seeds: &[&[u8]] = #signer_seeds_tokens; + if signer_seeds.is_empty() { + anchor_lang::solana_program::program::invoke(&mint_action_ix, &account_infos)?; + } else { + anchor_lang::solana_program::program::invoke_signed(&mint_action_ix, &account_infos, &[signer_seeds])?; + } + } + + Ok(true) + } +} + +/// Generate LightPreInit body for PDAs only (no mints) +/// After this, compressed addresses are registered +fn generate_pre_init_pdas_only( + parsed: &ParsedCompressibleStruct, + params_ident: &syn::Ident, + fee_payer: &TokenStream, + compression_config: &TokenStream, +) -> TokenStream { + let (compress_blocks, new_addr_idents) = + generate_pda_compress_blocks(&parsed.rentfree_fields, params_ident); + let rentfree_count = parsed.rentfree_fields.len() as u8; + + quote! { + // Build CPI accounts (no CPI context needed for PDAs-only) + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( + &self.#fee_payer, + _remaining, + crate::LIGHT_CPI_SIGNER, + ); + + // Load compression config + let compression_config_data = light_sdk::compressible::CompressibleConfig::load_checked( + &self.#compression_config, + &crate::ID + )?; + + // Collect compressed infos for all rentfree accounts + let mut all_compressed_infos = Vec::with_capacity(#rentfree_count as usize); + #(#compress_blocks)* + + // Execute Light System Program CPI directly with proof + use light_sdk::cpi::{InvokeLightSystemProgram, LightCpiInstruction}; + light_sdk::cpi::v2::LightSystemProgramCpi::new_cpi( + crate::LIGHT_CPI_SIGNER, + #params_ident.create_accounts_proof.proof.clone() + ) + .with_new_addresses(&[#(#new_addr_idents),*]) + .with_account_infos(&all_compressed_infos) + .invoke(cpi_accounts)?; + + Ok(true) + } +} + +/// Generate compression blocks for PDA fields +fn generate_pda_compress_blocks( + fields: &[RentFreeField], + _params_ident: &syn::Ident, +) -> (Vec, Vec) { + let mut blocks = Vec::new(); + let mut addr_idents = Vec::new(); + + for (idx, field) in fields.iter().enumerate() { + let idx_lit = idx as u8; + let ident = &field.ident; + let addr_tree_info = &field.address_tree_info; + let output_tree = &field.output_tree; + let acc_ty_path = extract_inner_account_type(&field.ty); + + let new_addr_params_ident = format_ident!("__new_addr_params_{}", idx); + let compressed_infos_ident = format_ident!("__compressed_infos_{}", idx); + let address_ident = format_ident!("__address_{}", idx); + let account_info_ident = format_ident!("__account_info_{}", idx); + let account_key_ident = format_ident!("__account_key_{}", idx); + let account_data_ident = format_ident!("__account_data_{}", idx); + + // Generate correct deref pattern: ** for Box>, * for Account + let deref_expr = if field.is_boxed { + quote! { &mut **self.#ident } + } else { + quote! { &mut *self.#ident } + }; + + addr_idents.push(quote! { #new_addr_params_ident }); + + blocks.push(quote! { + // Get account info early before any mutable borrows + let #account_info_ident = self.#ident.to_account_info(); + let #account_key_ident = #account_info_ident.key.to_bytes(); + + let #new_addr_params_ident = { + let tree_info = &#addr_tree_info; + light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked { + seed: #account_key_ident, + address_merkle_tree_account_index: tree_info.address_merkle_tree_pubkey_index, + address_queue_account_index: tree_info.address_queue_pubkey_index, + address_merkle_tree_root_index: tree_info.root_index, + assigned_to_account: true, + assigned_account_index: #idx_lit, + } + }; + + // Derive the compressed address + let #address_ident = light_compressed_account::address::derive_address( + &#new_addr_params_ident.seed, + &cpi_accounts + .get_tree_account_info(#new_addr_params_ident.address_merkle_tree_account_index as usize)? + .key() + .to_bytes(), + &crate::ID.to_bytes(), + ); + + // Get mutable reference to inner account data + let #account_data_ident = #deref_expr; + + let #compressed_infos_ident = light_sdk::compressible::prepare_compressed_account_on_init::<#acc_ty_path>( + &#account_info_ident, + #account_data_ident, + &compression_config_data, + #address_ident, + #new_addr_params_ident, + #output_tree, + &cpi_accounts, + &compression_config_data.address_space, + false, // at init, we do not compress_and_close the pda, we just "register" the empty compressed account with the derived address. + )?; + all_compressed_infos.push(#compressed_infos_ident); + }); + } + + (blocks, addr_idents) +} + +/// Extract the inner type T from Account<'info, T> or Box> +fn extract_inner_account_type(ty: &syn::Type) -> TokenStream { + match ty { + syn::Type::Path(type_path) => { + let path = &type_path.path; + if let Some(segment) = path.segments.last() { + let ident_str = segment.ident.to_string(); + + if ident_str == "Account" { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + for arg in &args.args { + if let syn::GenericArgument::Type(inner_ty) = arg { + return quote! { #inner_ty }; + } + } + } + } + + if ident_str == "Box" { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { + return extract_inner_account_type(inner); + } + } + } + } + quote! { #ty } + } + _ => quote! { #ty }, + } +} diff --git a/sdk-libs/macros/src/finalize/instruction.rs b/sdk-libs/macros/src/finalize/instruction.rs new file mode 100644 index 0000000000..357f9aa481 --- /dev/null +++ b/sdk-libs/macros/src/finalize/instruction.rs @@ -0,0 +1,115 @@ +//! The #[light_instruction] attribute macro. +//! +//! Wraps instruction handlers to automatically call: +//! - `light_pre_init()` at the START (creates mints via CPI context write) +//! - `light_finalize()` at the END (compresses PDAs and executes with proof) +//! +//! This two-phase design allows mints to be used during the instruction body. + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned, + Ident, ItemFn, +}; + +/// Arguments for #[light_instruction] or #[light_instruction(params_name)] +/// +/// If no params_name is provided, defaults to `params`. +pub struct LightInstructionArgs { + pub params_ident: Ident, +} + +impl Parse for LightInstructionArgs { + fn parse(input: ParseStream) -> syn::Result { + // If empty, default to "params" + if input.is_empty() { + return Ok(Self { + params_ident: Ident::new("params", proc_macro2::Span::call_site()), + }); + } + // Otherwise parse the identifier: #[light_instruction(my_params)] + let params_ident: Ident = input.parse()?; + Ok(Self { params_ident }) + } +} + +/// Generate the wrapped instruction function +pub fn light_instruction_impl( + args: LightInstructionArgs, + item: ItemFn, +) -> Result { + let params_ident = &args.params_ident; + let fn_vis = &item.vis; + let fn_sig = &item.sig; + let fn_block = &item.block; + let fn_attrs = &item.attrs; + + // Validate that the function has a Context parameter named `ctx` + // and a parameter matching params_ident + let mut has_ctx = false; + let mut has_params = false; + + for input in &fn_sig.inputs { + if let syn::FnArg::Typed(pat_type) = input { + if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { + if pat_ident.ident == "ctx" { + has_ctx = true; + } + if &pat_ident.ident == params_ident { + has_params = true; + } + } + } + } + + if !has_ctx { + return Err(syn::Error::new( + fn_sig.span(), + "light_instruction requires a parameter named `ctx` (the Anchor Context)", + )); + } + + if !has_params { + return Err(syn::Error::new( + params_ident.span(), + format!( + "parameter `{}` not found in function signature", + params_ident + ), + )); + } + + // Generate the wrapped function with two-phase compression: + // 1. light_pre_init() at START - creates mints via CPI context write + // 2. light_finalize() at END - compresses PDAs and executes with proof + Ok(quote! { + #(#fn_attrs)* + #fn_vis #fn_sig { + // Phase 1: Pre-init mints (writes to CPI context, does NOT execute yet) + // This allows mint accounts to be used during the instruction body + use light_sdk::compressible::{LightPreInit, LightFinalize}; + let __has_pre_init = ctx.accounts.light_pre_init(ctx.remaining_accounts, &#params_ident) + .map_err(|e| { + let pe: solana_program_error::ProgramError = e.into(); + pe + })?; + + // Execute the original handler body in a closure + let __light_handler_result = (|| #fn_block)(); + + // Phase 2: On success, finalize compression (compresses PDAs + executes proof) + // This runs BEFORE Anchor's exit() hook which serializes account data + if __light_handler_result.is_ok() { + ctx.accounts.light_finalize(ctx.remaining_accounts, &#params_ident, __has_pre_init) + .map_err(|e| { + let pe: solana_program_error::ProgramError = e.into(); + pe + })?; + } + + __light_handler_result + } + }) +} diff --git a/sdk-libs/macros/src/finalize/mod.rs b/sdk-libs/macros/src/finalize/mod.rs new file mode 100644 index 0000000000..b1daf31d8e --- /dev/null +++ b/sdk-libs/macros/src/finalize/mod.rs @@ -0,0 +1,18 @@ +//! RentFree derive macro and light_instruction attribute macro. +//! +//! This module provides: +//! - `#[derive(RentFree)]` - Generates the LightFinalize trait impl for accounts structs +//! with fields marked `#[rentfree(...)]` +//! - `#[light_instruction(params)]` - Attribute macro that auto-calls light_finalize at end of handler + +mod codegen; +pub mod instruction; +mod parse; + +use proc_macro2::TokenStream; +use syn::DeriveInput; + +pub fn derive_light_finalize(input: DeriveInput) -> Result { + let parsed = parse::parse_compressible_struct(&input)?; + Ok(codegen::generate_finalize_impl(&parsed)) +} diff --git a/sdk-libs/macros/src/finalize/parse.rs b/sdk-libs/macros/src/finalize/parse.rs new file mode 100644 index 0000000000..6296cf3892 --- /dev/null +++ b/sdk-libs/macros/src/finalize/parse.rs @@ -0,0 +1,389 @@ +//! Parsing logic for #[rentfree(...)] and #[light_mint(...)] attributes. + +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + DeriveInput, Error, Expr, Ident, Token, Type, +}; + +/// Parsed representation of a struct with rentfree and light_mint fields. +pub struct ParsedCompressibleStruct { + pub struct_name: Ident, + pub generics: syn::Generics, + pub rentfree_fields: Vec, + pub light_mint_fields: Vec, + pub instruction_args: Option>, + pub fee_payer_field: Option, + pub compression_config_field: Option, + /// CToken compressible config account (for decompress mint) + pub ctoken_config_field: Option, + /// CToken rent sponsor account (for decompress mint) + pub ctoken_rent_sponsor_field: Option, + /// CToken program account (for decompress mint CPI) + pub ctoken_program_field: Option, + /// CToken CPI authority PDA (for decompress mint CPI) + pub ctoken_cpi_authority_field: Option, +} + +/// A field marked with #[rentfree(...)] +pub struct RentFreeField { + pub ident: Ident, + pub ty: Type, + pub address_tree_info: Expr, + pub output_tree: Expr, + /// True if the field is Box>, false if Account + pub is_boxed: bool, +} + +/// A field marked with #[light_mint(...)] +pub struct LightMintField { + /// The field name where #[light_mint] is attached (CMint account) + pub field_ident: Ident, + /// The mint_signer field (AccountInfo that seeds the mint PDA) + pub mint_signer: Expr, + /// The authority for mint operations + pub authority: Expr, + /// Decimals for the mint + pub decimals: Expr, + /// Address tree info expression + pub address_tree_info: Expr, + /// Optional freeze authority + pub freeze_authority: Option, + /// Signer seeds for the mint_signer PDA (required if mint_signer is a PDA) + pub signer_seeds: Option, + /// Rent payment epochs for decompression (default: 2) + pub rent_payment: Option, + /// Write top-up lamports for decompression (default: 0) + pub write_top_up: Option, +} + +/// Instruction argument from #[instruction(...)] +pub struct InstructionArg { + pub name: Ident, + pub ty: Type, +} + +/// Arguments inside #[rentfree(...)] +struct RentFreeArgs { + address_tree_info: Option, + output_tree: Option, +} + +impl Parse for RentFreeArgs { + fn parse(input: ParseStream) -> syn::Result { + let mut args = RentFreeArgs { + address_tree_info: None, + output_tree: None, + }; + + let content: Punctuated = Punctuated::parse_terminated(input)?; + + for arg in content { + match arg.name.to_string().as_str() { + "address_tree_info" => args.address_tree_info = Some(arg.value), + "output_tree" => args.output_tree = Some(arg.value), + other => { + return Err(Error::new( + arg.name.span(), + format!("unknown rentfree attribute: {}", other), + )) + } + } + } + + Ok(args) + } +} + +/// Arguments inside #[light_mint(...)] +struct LightMintArgs { + mint_signer: Option, + authority: Option, + decimals: Option, + address_tree_info: Option, + freeze_authority: Option, + signer_seeds: Option, + rent_payment: Option, + write_top_up: Option, +} + +impl Parse for LightMintArgs { + fn parse(input: ParseStream) -> syn::Result { + let mut args = LightMintArgs { + mint_signer: None, + authority: None, + decimals: None, + address_tree_info: None, + freeze_authority: None, + signer_seeds: None, + rent_payment: None, + write_top_up: None, + }; + + let content: Punctuated = Punctuated::parse_terminated(input)?; + + for arg in content { + match arg.name.to_string().as_str() { + "mint_signer" => args.mint_signer = Some(arg.value), + "authority" => args.authority = Some(arg.value), + "decimals" => args.decimals = Some(arg.value), + "address_tree_info" => args.address_tree_info = Some(arg.value), + "freeze_authority" => args.freeze_authority = Some(arg.value), + "signer_seeds" => args.signer_seeds = Some(arg.value), + "rent_payment" => args.rent_payment = Some(arg.value), + "write_top_up" => args.write_top_up = Some(arg.value), + other => { + return Err(Error::new( + arg.name.span(), + format!("unknown light_mint attribute: {}", other), + )) + } + } + } + + Ok(args) + } +} + +/// Generic key = value argument parser +struct KeyValueArg { + name: Ident, + value: Expr, +} + +impl Parse for KeyValueArg { + fn parse(input: ParseStream) -> syn::Result { + let name: Ident = input.parse()?; + input.parse::()?; + let value: Expr = input.parse()?; + Ok(KeyValueArg { name, value }) + } +} + +/// A single instruction argument: `name: Type` +struct InstructionArgParsed { + name: Ident, + _colon: Token![:], + ty: Type, +} + +impl Parse for InstructionArgParsed { + fn parse(input: ParseStream) -> syn::Result { + Ok(InstructionArgParsed { + name: input.parse()?, + _colon: input.parse()?, + ty: input.parse()?, + }) + } +} + +/// Parse #[instruction(...)] attribute from struct +fn parse_instruction_attr(attrs: &[syn::Attribute]) -> Option> { + for attr in attrs { + if attr.path().is_ident("instruction") { + if let Ok(args) = attr.parse_args_with(|input: ParseStream| { + let content: Punctuated = + Punctuated::parse_terminated(input)?; + Ok(content + .into_iter() + .map(|arg| InstructionArg { + name: arg.name, + ty: arg.ty, + }) + .collect::>()) + }) { + return Some(args); + } + } + } + None +} + +/// Check if a type is Account<...> or Box> +fn extract_account_type(ty: &Type) -> Option<(bool, &syn::Path)> { + match ty { + Type::Path(type_path) => { + let path = &type_path.path; + if let Some(segment) = path.segments.last() { + let ident_str = segment.ident.to_string(); + if ident_str == "Account" { + return Some((false, path)); + } + if ident_str == "Box" { + // Check for Box> + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + if let Type::Path(inner_path) = inner_ty { + if let Some(inner_seg) = inner_path.path.segments.last() { + if inner_seg.ident == "Account" { + return Some((true, &inner_path.path)); + } + } + } + } + } + } + } + None + } + _ => None, + } +} + +/// Parse a struct to extract rentfree and light_mint fields +pub fn parse_compressible_struct(input: &DeriveInput) -> Result { + let struct_name = input.ident.clone(); + let generics = input.generics.clone(); + + let instruction_args = parse_instruction_attr(&input.attrs); + + let fields = match &input.data { + syn::Data::Struct(data) => match &data.fields { + syn::Fields::Named(fields) => &fields.named, + _ => return Err(Error::new_spanned(input, "expected named fields")), + }, + _ => return Err(Error::new_spanned(input, "expected struct")), + }; + + let mut rentfree_fields = Vec::new(); + let mut light_mint_fields = Vec::new(); + let mut fee_payer_field = None; + let mut compression_config_field = None; + let mut ctoken_config_field = None; + let mut ctoken_rent_sponsor_field = None; + let mut ctoken_program_field = None; + let mut ctoken_cpi_authority_field = None; + + for field in fields { + let field_ident = field.ident.clone().unwrap(); + let field_name = field_ident.to_string(); + + // Track special fields by name + if field_name == "fee_payer" || field_name == "payer" || field_name == "creator" { + fee_payer_field = Some(field_ident.clone()); + } + if field_name == "compression_config" { + compression_config_field = Some(field_ident.clone()); + } + // Track ctoken-related fields for decompress mint + if field_name == "ctoken_compressible_config" + || field_name == "ctoken_config" + || field_name == "light_token_config_account" + { + ctoken_config_field = Some(field_ident.clone()); + } + if field_name == "ctoken_rent_sponsor" || field_name == "light_token_rent_sponsor" { + ctoken_rent_sponsor_field = Some(field_ident.clone()); + } + if field_name == "ctoken_program" || field_name == "light_token_program" { + ctoken_program_field = Some(field_ident.clone()); + } + if field_name == "ctoken_cpi_authority" + || field_name == "light_token_program_cpi_authority" + || field_name == "compress_token_program_cpi_authority" + { + ctoken_cpi_authority_field = Some(field_ident.clone()); + } + + // Look for #[rentfree] or #[rentfree(...)] attribute + for attr in &field.attrs { + if attr.path().is_ident("rentfree") { + // Handle both #[rentfree] and #[rentfree(...)] + let args: RentFreeArgs = match &attr.meta { + syn::Meta::Path(_) => { + // No parentheses: #[rentfree] + RentFreeArgs { + address_tree_info: None, + output_tree: None, + } + } + syn::Meta::List(_) => { + // Has parentheses: #[rentfree(...)] + attr.parse_args()? + } + syn::Meta::NameValue(_) => { + return Err(Error::new_spanned( + attr, + "expected #[rentfree] or #[rentfree(...)]", + )); + } + }; + + // Use defaults if not specified: + // - address_tree_info defaults to params.create_accounts_proof.address_tree_info + // - output_tree defaults to params.create_accounts_proof.output_state_tree_index + let address_tree_info = args.address_tree_info.unwrap_or_else(|| { + syn::parse_quote!(params.create_accounts_proof.address_tree_info) + }); + let output_tree = args.output_tree.unwrap_or_else(|| { + syn::parse_quote!(params.create_accounts_proof.output_state_tree_index) + }); + + // Validate this is an Account type and check if it's boxed + let (is_boxed, _) = extract_account_type(&field.ty).ok_or_else(|| { + Error::new_spanned( + &field.ty, + "#[rentfree] can only be applied to Account<...> fields", + ) + })?; + + rentfree_fields.push(RentFreeField { + ident: field_ident.clone(), + ty: field.ty.clone(), + address_tree_info, + output_tree, + is_boxed, + }); + break; + } + + // Look for #[light_mint(...)] attribute + if attr.path().is_ident("light_mint") { + let args: LightMintArgs = attr.parse_args()?; + + // Validate required fields + let mint_signer = args + .mint_signer + .ok_or_else(|| Error::new_spanned(attr, "light_mint requires mint_signer"))?; + let authority = args + .authority + .ok_or_else(|| Error::new_spanned(attr, "light_mint requires authority"))?; + let decimals = args + .decimals + .ok_or_else(|| Error::new_spanned(attr, "light_mint requires decimals"))?; + + // address_tree_info defaults to params.create_accounts_proof.address_tree_info + let address_tree_info = args.address_tree_info.unwrap_or_else(|| { + syn::parse_quote!(params.create_accounts_proof.address_tree_info) + }); + + light_mint_fields.push(LightMintField { + field_ident: field_ident.clone(), + mint_signer, + authority, + decimals, + address_tree_info, + freeze_authority: args.freeze_authority, + signer_seeds: args.signer_seeds, + rent_payment: args.rent_payment, + write_top_up: args.write_top_up, + }); + break; + } + } + } + + Ok(ParsedCompressibleStruct { + struct_name, + generics, + rentfree_fields, + light_mint_fields, + instruction_args, + fee_payer_field, + compression_config_field, + ctoken_config_field, + ctoken_rent_sponsor_field, + ctoken_program_field, + ctoken_cpi_authority_field, + }) +} diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 4161dbea34..e541019b2a 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -1,148 +1,18 @@ extern crate proc_macro; -use accounts::{process_light_accounts, process_light_system_accounts}; use discriminator::discriminator; use hasher::{derive_light_hasher, derive_light_hasher_sha}; use proc_macro::TokenStream; -use syn::{parse_macro_input, DeriveInput, ItemStruct}; -use traits::process_light_traits; +use syn::{parse_macro_input, DeriveInput, ItemFn, ItemStruct}; use utils::into_token_stream; mod account; -mod accounts; mod compressible; mod discriminator; +mod finalize; mod hasher; -mod program; mod rent_sponsor; -mod traits; mod utils; -/// Adds required fields to your anchor instruction for applying a zk-compressed -/// state transition. -/// -/// ## Usage -/// Add `#[light_system_accounts]` to your struct. Ensure it's applied before Anchor's -/// `#[derive(Accounts)]` and Light's `#[derive(LightTraits)]`. -/// -/// ## Example -/// Note: You will have to build your program IDL using Anchor's `idl-build` -/// feature, otherwise your IDL won't include these accounts. -/// ```ignore -/// use anchor_lang::prelude::*; -/// use light_sdk_macros::light_system_accounts; -/// -/// declare_id!("Fg6PaFpoGXkYsidMpWxTWKGNpKK39H3UKo7wjRZnq89u"); -/// -/// #[program] -/// pub mod my_program { -/// use super::*; -/// } -/// -/// #[light_system_accounts] -/// #[derive(Accounts)] -/// pub struct ExampleInstruction<'info> { -/// pub my_program: Program<'info, MyProgram>, -/// } -/// ``` -/// This will expand to add the following fields to your struct: -/// - `light_system_program`: Verifies and applies zk-compression -/// state transitions. -/// - `registered_program_pda`: A light protocol PDA to authenticate -/// state tree updates. -/// - `noop_program`: The SPL noop program to write -/// compressed-account state as calldata to -/// the Solana ledger. -/// - `account_compression_authority`: The authority for account compression -/// operations. -/// - `account_compression_program`: Called by light_system_program. Updates -/// state trees. -/// - `system_program`: The Solana System program. -#[proc_macro_attribute] -pub fn light_system_accounts(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - into_token_stream(process_light_system_accounts(input)) -} - -#[proc_macro_attribute] -pub fn light_accounts(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - into_token_stream(process_light_accounts(input)) -} - -#[proc_macro_derive(LightAccounts, attributes(light_account))] -pub fn light_accounts_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - into_token_stream(accounts::process_light_accounts_derive(input)) -} - -/// Implements traits on the given struct required for invoking The Light system -/// program via CPI. -/// -/// ## Usage -/// Add `#[derive(LightTraits)]` to your struct which specifies the accounts -/// required for your Anchor program instruction. Specify the attributes -/// `self_program`, `fee_payer`, `authority`, and optionally `cpi_context` to -/// the relevant fields. -/// -/// ### Attributes -/// - `self_program`: Marks the field that represents the program invoking the -/// light system program, i.e. your program. You need to -/// list your program as part of the struct. -/// - `fee_payer`: Marks the field that represents the account responsible -/// for paying transaction fees. (Signer) -/// -/// - `authority`: TODO: explain authority. -/// - `cpi_context`: TODO: explain cpi_context. -/// -/// ### Required accounts (must specify exact name). -/// -/// - `light_system_program`: Light systemprogram. verifies & applies -/// compression state transitions. -/// - `registered_program_pda`: Light Systemprogram PDA -/// - `noop_program`: SPL noop program -/// - `account_compression_authority`: TODO: explain. -/// - `account_compression_program`: Account Compression program. -/// - `system_program`: The Solana Systemprogram. -/// -/// ### Example -/// ```ignore -/// use anchor_lang::prelude::*; -/// use light_sdk_macros::LightTraits; -/// -/// declare_id!("Fg6PaFpoGXkYsidMpWxTWKGNpKK39H3UKo7wjRZnq89u"); -/// -/// #[program] -/// pub mod my_program { -/// use super::*; -/// } -/// -/// #[derive(Accounts, LightTraits)] -/// pub struct ExampleInstruction<'info> { -/// #[self_program] -/// pub my_program: Program<'info, MyProgram>, -/// #[fee_payer] -/// pub payer: Signer<'info>, -/// #[authority] -/// pub user: AccountInfo<'info>, -/// #[cpi_context] -/// pub cpi_context_account: AccountInfo<'info>, -/// pub light_system_program: AccountInfo<'info>, -/// pub registered_program_pda: AccountInfo<'info>, -/// pub noop_program: AccountInfo<'info>, -/// pub account_compression_authority: AccountInfo<'info>, -/// pub account_compression_program: AccountInfo<'info>, -/// pub system_program: Program<'info, System>, -/// } -/// ``` -#[proc_macro_derive( - LightTraits, - attributes(self_program, fee_payer, authority, cpi_context) -)] -pub fn light_traits_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - into_token_stream(process_light_traits(input)) -} - #[proc_macro_derive(LightDiscriminator)] pub fn light_discriminator(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemStruct); @@ -224,13 +94,6 @@ pub fn light_hasher_sha(input: TokenStream) -> TokenStream { into_token_stream(derive_light_hasher_sha(input)) } -/// Alias of `LightHasher`. -#[proc_macro_derive(DataHasher, attributes(skip, hash))] -pub fn data_hasher(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - into_token_stream(derive_light_hasher_sha(input)) -} - /// Automatically implements the HasCompressionInfo trait for structs that have a /// `compression_info: Option` field. /// @@ -305,38 +168,39 @@ pub fn compress_as_derive(input: TokenStream) -> TokenStream { into_token_stream(compressible::traits::derive_compress_as(input)) } -/// Adds compressible account support with automatic seed generation. +/// Auto-discovering rent-free program macro that reads external module files. /// -/// This macro generates everything needed for compressible accounts: -/// - CompressedAccountVariant enum with all trait implementations -/// - Compress and decompress instructions with auto-generated seed derivation -/// - CTokenSeedProvider implementation for token accounts -/// - All required account structs and functions +/// This macro automatically discovers #[rentfree] fields in Accounts structs +/// by reading external module files. No explicit type list needed! /// -/// ## Usage +/// Usage: /// ```ignore -/// use anchor_lang::prelude::*; -/// use light_sdk_macros::add_compressible_instructions; -/// -/// declare_id!("Fg6PaFpoGXkYsidMpWxTWKGNpKK39H3UKo7wjRZnq89u"); -/// -/// #[add_compressible_instructions( -/// UserRecord = ("user_record", data.owner), -/// GameSession = ("game_session", data.session_id.to_le_bytes()), -/// CTokenSigner = (is_token, "ctoken_signer", ctx.fee_payer, ctx.mint) -/// )] +/// #[rentfree_program] /// #[program] /// pub mod my_program { -/// use super::*; -/// // Your regular instructions here - everything else is auto-generated! -/// // CTokenAccountVariant enum is automatically generated with: -/// // - CTokenSigner = 0 +/// pub mod instruction_accounts; // Macro reads this file! +/// pub mod state; +/// +/// use instruction_accounts::*; +/// use state::*; +/// +/// #[light_instruction] +/// pub fn create_user(ctx: Context, params: Params) -> Result<()> { +/// // ... +/// } /// } /// ``` +/// +/// The macro: +/// 1. Scans the crate's `src/` directory for `#[derive(Accounts)]` structs +/// 2. Extracts seeds from `#[account(seeds = [...])]` on `#[rentfree]` fields +/// 3. Generates all necessary types, enums, and instruction handlers +/// +/// Seeds are declared ONCE in Anchor attributes - no duplication! #[proc_macro_attribute] -pub fn add_compressible_instructions(args: TokenStream, input: TokenStream) -> TokenStream { +pub fn rentfree_program(args: TokenStream, input: TokenStream) -> TokenStream { let module = syn::parse_macro_input!(input as syn::ItemMod); - into_token_stream(compressible::instructions::add_compressible_instructions( + into_token_stream(compressible::instructions::compressible_program_impl( args.into(), module, )) @@ -421,94 +285,52 @@ pub fn compressible_pack(input: TokenStream) -> TokenStream { into_token_stream(compressible::pack_unpack::derive_compressible_pack(input)) } -// DEPRECATED: compressed_account_variant macro is now integrated into add_compressible_instructions -// Use add_compressible_instructions instead for complete automation - -/// Generates complete compressible instructions with auto-generated seed derivation. +/// Consolidates all required traits for rent-free accounts into a single derive. /// -/// This is a drop-in replacement for manual decompress_accounts_idempotent and -/// compress_accounts_idempotent instructions. It reads #[light_seeds(...)] attributes -/// from account types and generates complete instructions with inline seed derivation. +/// This macro is equivalent to deriving: +/// - `LightHasherSha` (SHA256/ShaFlat hashing - type 3) +/// - `LightDiscriminator` (unique discriminator) +/// - `Compressible` (HasCompressionInfo + CompressAs + Size + CompressedInitSpace) +/// - `CompressiblePack` (Pack + Unpack + Packed struct generation) /// /// ## Example /// -/// Add #[light_seeds(...)] to your account types: /// ```ignore -/// use light_sdk_macros::{Compressible, CompressiblePack}; +/// use light_sdk_macros::Light; +/// use light_sdk::compressible::CompressionInfo; /// use solana_pubkey::Pubkey; /// -/// #[derive(Compressible, CompressiblePack)] -/// #[light_seeds(b"user_record", owner.as_ref())] +/// #[derive(Default, Debug, InitSpace, Light)] +/// #[account] /// pub struct UserRecord { /// pub owner: Pubkey, -/// // ... -/// } -/// -/// #[derive(Compressible, CompressiblePack)] -/// #[light_seeds(b"game_session", session_id.to_le_bytes().as_ref())] -/// pub struct GameSession { -/// pub session_id: u64, -/// // ... +/// #[max_len(32)] +/// pub name: String, +/// pub score: u64, +/// pub compression_info: Option, /// } /// ``` /// -/// Then generate complete instructions: +/// This is equivalent to: /// ```ignore -/// # macro_rules! compressed_account_variant_with_instructions { ($($t:ty),*) => {} } -/// compressed_account_variant_with_instructions!(UserRecord, GameSession, PlaceholderRecord); +/// #[derive(Default, Debug, InitSpace, LightHasherSha, LightDiscriminator, Compressible, CompressiblePack)] +/// #[account] +/// pub struct UserRecord { ... } /// ``` /// -/// This generates: -/// - CompressedAccountVariant enum + all trait implementations -/// - Complete decompress_accounts_idempotent instruction with auto-generated seed derivation -/// - Complete compress_accounts_idempotent instruction with auto-generated seed derivation -/// - CompressedAccountData struct -/// -/// The generated instructions automatically handle seed derivation for each account type -/// without requiring manual seed function calls. -/// -/// Derive DecompressContext trait implementation. -/// -/// This generates the full DecompressContext trait implementation for -/// decompression account structs. Can be used standalone or is automatically -/// used by add_compressible_instructions. -/// /// ## Attributes -/// - `#[pda_types(Type1, Type2, ...)]` - List of PDA account types -/// - `#[token_variant(CTokenAccountVariant)]` - The token variant enum name /// -/// ## Example +/// - `#[compress_as(...)]` - Optional: specify field values to reset during compression /// -/// ```ignore -/// use anchor_lang::prelude::*; -/// use light_sdk_macros::DecompressContext; -/// -/// declare_id!("Fg6PaFpoGXkYsidMpWxTWKGNpKK39H3UKo7wjRZnq89u"); -/// -/// struct UserRecord; -/// struct GameSession; -/// enum CTokenAccountVariant {} +/// ## Notes /// -/// #[derive(Accounts, DecompressContext)] -/// #[pda_types(UserRecord, GameSession)] -/// #[token_variant(CTokenAccountVariant)] -/// pub struct DecompressAccountsIdempotent<'info> { -/// #[account(mut)] -/// pub fee_payer: Signer<'info>, -/// pub config: AccountInfo<'info>, -/// #[account(mut)] -/// pub rent_sponsor: Signer<'info>, -/// #[account(mut)] -/// pub ctoken_rent_sponsor: AccountInfo<'info>, -/// pub ctoken_program: UncheckedAccount<'info>, -/// pub ctoken_cpi_authority: UncheckedAccount<'info>, -/// pub ctoken_config: UncheckedAccount<'info>, -/// } -/// ``` -#[proc_macro_derive(DecompressContext, attributes(pda_types, token_variant))] -pub fn derive_decompress_context(input: TokenStream) -> TokenStream { +/// - The `compression_info` field is auto-detected and handled (no `#[skip]` needed) +/// - SHA256 (ShaFlat) hashes the entire serialized struct (no `#[hash]` needed) +/// - The struct must have a `compression_info: Option` field +#[proc_macro_derive(Light, attributes(compress_as))] +pub fn light(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(compressible::decompress_context::derive_decompress_context( + into_token_stream(compressible::light_compressible::derive_light_compressible( input, )) } @@ -546,30 +368,134 @@ pub fn derive_light_rent_sponsor_pda(input: TokenStream) -> TokenStream { pub fn derive_light_rent_sponsor(input: TokenStream) -> TokenStream { rent_sponsor::derive_light_rent_sponsor(input) } -/// Generates a Light program for the given module. + +/// Generates `RentFree` trait implementation for rent-free accounts and light-mints. /// -/// ## Example +/// This derive macro works alongside Anchor's `#[derive(Accounts)]` to add +/// compression finalize logic for: +/// - Accounts marked with `#[rentfree]` (rent-free PDAs) +/// - Accounts marked with `#[rentfree_token(...)]` (rent-free token accounts) +/// - Accounts marked with `#[light_mint(...)]` (light-mint creation) +/// +/// The trait is defined in `light_sdk::compressible::LightFinalize`. +/// +/// ## Usage - Rent-free PDAs /// /// ```ignore -/// use light_sdk_macros::light_program; -/// use anchor_lang::prelude::*; +/// #[derive(Accounts, RentFree)] +/// #[instruction(params: CompressionParams)] +/// pub struct CreateRentFree<'info> { +/// #[account(mut)] +/// pub fee_payer: Signer<'info>, +/// +/// #[account( +/// init, payer = fee_payer, space = 8 + MyData::INIT_SPACE, +/// seeds = [b"my_data", authority.key().as_ref()], +/// bump +/// )] +/// #[rentfree] +/// pub my_account: Account<'info, MyData>, +/// +/// /// CHECK: Compression config +/// pub compression_config: AccountInfo<'info>, +/// } +/// ``` /// -/// declare_id!("Fg6PaFpoGXkYsidMpWxTWKGNpKK39H3UKo7wjRZnq89u"); +/// ## Usage - Rent-free Token Accounts /// -/// #[derive(Accounts)] -/// pub struct MyInstruction {} +/// ```ignore +/// #[derive(Accounts, RentFree)] +/// pub struct CreateVault<'info> { +/// #[account( +/// mut, +/// seeds = [b"vault", cmint.key().as_ref()], +/// bump +/// )] +/// #[rentfree_token(Vault, authority = [b"vault_authority"])] +/// pub vault: UncheckedAccount<'info>, +/// } +/// ``` /// -/// #[light_program] -/// pub mod my_program { -/// use super::*; -/// pub fn my_instruction(ctx: Context) -> Result<()> { -/// // Your instruction logic here -/// Ok(()) -/// } +/// ## Usage - Light Mints +/// +/// ```ignore +/// #[derive(Accounts, RentFree)] +/// #[instruction(params: MintParams)] +/// pub struct CreateMint<'info> { +/// #[account(mut)] +/// pub fee_payer: Signer<'info>, +/// +/// #[account(mut)] +/// #[light_mint( +/// mint_signer = mint_signer, +/// authority = authority, +/// decimals = 9, +/// signer_seeds = &[...] +/// )] +/// pub mint: UncheckedAccount<'info>, +/// +/// pub mint_signer: Signer<'info>, +/// pub authority: Signer<'info>, /// } /// ``` +/// +/// ## Requirements +/// +/// Your program must define: +/// - `LIGHT_CPI_SIGNER`: CPI signer pubkey constant +/// - `ID`: Program ID (from declare_id!) +/// +/// The struct should have fields named `fee_payer` (or `payer`) and `compression_config`. +#[proc_macro_derive( + RentFree, + attributes(rentfree, rentfree_token, light_mint, instruction) +)] +pub fn rent_free_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + into_token_stream(finalize::derive_light_finalize(input)) +} + +/// Attribute macro that auto-calls `light_finalize()` at end of instruction handler. +/// +/// This macro wraps your instruction handler to automatically call the +/// `LightFinalize::light_finalize()` method before returning, which executes +/// the compression CPIs. This runs BEFORE Anchor's `exit()` hook. +/// +/// ## Usage +/// +/// ```ignore +/// use anchor_lang::prelude::*; +/// use light_sdk_macros::light_instruction; +/// +/// // The argument is the name of the parameter containing compression data +/// #[light_instruction(params)] +/// pub fn create_compressible(ctx: Context, params: CompressionParams) -> Result<()> { +/// // Your business logic +/// ctx.accounts.my_account.value = params.value; +/// +/// // light_finalize() is auto-called here before returning +/// Ok(()) +/// } +/// ``` +/// +/// ## How It Works +/// +/// The macro transforms your function to: +/// 1. Execute your original function body +/// 2. On success, call `ctx.accounts.light_finalize(ctx.remaining_accounts, ¶ms)` +/// 3. Return the result +/// +/// This ensures compression CPIs run after your logic but before Anchor serializes accounts. +/// +/// ## Important Notes +/// +/// - The `params` argument must match a parameter name in your function signature +/// - Your accounts struct must derive `LightFinalize` +/// - Use `?` operator for error handling (not explicit `return Err(...)`) +/// - Errors will skip `light_finalize` and propagate normally #[proc_macro_attribute] -pub fn light_program(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as syn::ItemMod); - into_token_stream(program::program(input)) +pub fn light_instruction(args: TokenStream, input: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as finalize::instruction::LightInstructionArgs); + let item = parse_macro_input!(input as ItemFn); + into_token_stream(finalize::instruction::light_instruction_impl(args, item)) } diff --git a/sdk-libs/macros/src/program.rs b/sdk-libs/macros/src/program.rs deleted file mode 100644 index 5df0375b1a..0000000000 --- a/sdk-libs/macros/src/program.rs +++ /dev/null @@ -1,301 +0,0 @@ -use std::{ - collections::HashMap, - ops::{Deref, DerefMut}, -}; - -use proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; -use syn::{ - parse_quote, visit_mut::VisitMut, Attribute, FnArg, GenericArgument, Ident, Item, ItemFn, - ItemMod, ItemStruct, Pat, PathArguments, Result, Stmt, Token, Type, -}; - -// A single instruction parameter provided as an argument to the Anchor program -// function. It consists of the name an the type, e.g.: `name: String`. -#[derive(Clone)] -struct InstructionParam { - name: Ident, - ty: Type, -} - -impl ToTokens for InstructionParam { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.name.to_tokens(tokens); - Token![:](self.name.span()).to_tokens(tokens); - self.ty.to_tokens(tokens); - } -} - -/// Map which stores instruction parameters for all instructions in the parsed -/// program. -/// -/// # Example -/// -/// For the program with the following instructions: -/// -/// ```ignore -/// #[light_program] -/// pub mode my_program { -/// use super::*; -/// -/// pub fn instruction_one( -/// ctx: LightContext<'_, '_, '_, 'info, InstructionOne<'info>>, -/// name: String, -/// num: u32, -/// ) -> Result<()> {} -/// -/// pub fn instruction_two( -/// ctx: LightContext<'_, '_, '_, 'info, InstructionTwo<'info>>, -/// num_one: u32, -/// num_two: u64, -/// ) -> Result<()> {} -/// } -/// ``` -/// -/// The mapping is going to look like: -/// -/// ```ignore -/// instruction_one -> - name: name -/// ty: String -/// - name: num -/// ty: u32 -/// -/// instruction_two -> - name: num_one -/// ty: u32 -/// - name: num_two -/// ty: u64 -/// ``` -#[derive(Default)] -struct InstructionParams(HashMap>); - -impl Deref for InstructionParams { - type Target = HashMap>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for InstructionParams { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -/// Implementation of `ToTokens` which allows to convert the -/// instruction-parameter mapping to structs, which we later use for packing -/// of parameters for convenient usage in `LightContext` extensions produced in -/// `accounts.rs` - precisely, in the `check_constraints` and -/// `derive_address_seeds` methods. -impl ToTokens for InstructionParams { - fn to_tokens(&self, tokens: &mut TokenStream) { - for (name, inputs) in self.0.iter() { - let name = Ident::new(name, Span::call_site()); - let strct: ItemStruct = parse_quote! { - pub struct #name { - #(#inputs),* - } - }; - strct.to_tokens(tokens); - } - } -} - -#[derive(Default)] -struct LightProgramTransform { - /// Mapping of instructions to their parameters in the program. - instruction_params: InstructionParams, -} - -impl VisitMut for LightProgramTransform { - fn visit_item_fn_mut(&mut self, i: &mut ItemFn) { - // Add `#[allow(clippy::too_many_arguments)]` attribute in case. We are - // injecting many arguments in this macro and they can easily go over - // the limit. - let clippy_attr: Attribute = parse_quote! { - #[allow(clippy::too_many_arguments)] - }; - i.attrs.push(clippy_attr); - - // Gather names instruction parameters (arguments other than `ctx`). - // They are going to be used to generate `Inputs*` structs. - let mut instruction_params = Vec::with_capacity(i.sig.inputs.len() - 1); - for input in i.sig.inputs.iter().skip(1) { - if let FnArg::Typed(input) = input { - if let Pat::Ident(ref pat_ident) = *input.pat { - instruction_params.push(InstructionParam { - name: pat_ident.ident.clone(), - ty: (*input.ty).clone(), - }); - } - } - } - - // Find the `ctx` argument. - let ctx_arg = i.sig.inputs.first_mut().unwrap(); - - // Retrieve the type of `ctx`. - let pat_type = match ctx_arg { - FnArg::Typed(pat_type) => pat_type, - _ => return, - }; - - // Get the last path segment of `ctx` type. - let type_path = match pat_type.ty.as_mut() { - Type::Path(type_path) => type_path, - _ => return, - }; - let ctx_segment = &mut type_path.path.segments.last_mut().unwrap(); - // If the `ctx` is of type `LightContext`, that means that the given - // instruction uses compressed accounts and we need to inject our code - // for handling them. - // Otherwise, stop processing the instruction and assume it's just a - // regular instruction using only regular accounts. - if ctx_segment.ident != "LightContext" { - return; - } - - // Swap the type of `ctx` to Anchor's `Context` to keep the instruction - // signature correct. We are going to inject the code converting it to - // `LightContext` later. - ctx_segment.ident = Ident::new("Context", Span::call_site()); - - // Figure out what's are the names of: - // - // - The struct with Anchor accounts (implementing `anchor_lang::Accounts`) - - // it's specified as the last generic argument in `ctx`, e.g. `MyInstruction`. - // - The struct with compressed accounts (implementing `LightAccounts`) - - // it's derived by adding the `Light` prefix to the previous struct name, - // e.g. `LightMyInstruction`. - let arguments = match &ctx_segment.arguments { - PathArguments::AngleBracketed(arguments) => arguments, - _ => return, - }; - let last_arg = arguments.args.last().unwrap(); - let last_arg_type = match last_arg { - GenericArgument::Type(last_arg_type) => last_arg_type, - _ => return, - }; - let last_arg_type_path = match last_arg_type { - Type::Path(last_arg_type_path) => last_arg_type_path, - _ => return, - }; - let accounts_segment = &last_arg_type_path.path.segments.last().unwrap(); - let accounts_ident = accounts_segment.ident.clone(); - let light_accounts_name = format!("Light{}", accounts_segment.ident); - let light_accounts_ident = Ident::new(&light_accounts_name, Span::call_site()); - - // Add the previously gathered instruction inputs to the mapping of - // instructions to their parameters (`self.instruction_inputs`). - let params_name = format!("Params{}", accounts_segment.ident); - self.instruction_params - .insert(params_name.clone(), instruction_params.clone()); - let inputs_ident = Ident::new(¶ms_name, Span::call_site()); - - // Inject an `inputs: Vec>` argument to all instructions. The - // purpose of that additional argument is passing compressed accounts. - let inputs_arg: FnArg = parse_quote! { inputs: Vec> }; - i.sig.inputs.insert(1, inputs_arg); - - // Inject Merkle context related arguments. - let proof_arg: FnArg = parse_quote! { proof: ::light_sdk::proof::CompressedProof }; - i.sig.inputs.insert(2, proof_arg); - let merkle_context_arg: FnArg = - parse_quote! { merkle_context: ::light_sdk::merkle_context::PackedMerkleContext }; - i.sig.inputs.insert(3, merkle_context_arg); - let merkle_tree_root_index_arg: FnArg = parse_quote! { merkle_tree_root_index: u16 }; - i.sig.inputs.insert(4, merkle_tree_root_index_arg); - let address_merkle_context_arg: FnArg = - parse_quote! { address_merkle_context: ::light_sdk::tree_info::PackedAddressTreeInfo }; - i.sig.inputs.insert(5, address_merkle_context_arg); - let address_merkle_tree_root_index_arg: FnArg = - parse_quote! { address_merkle_tree_root_index: u16 }; - i.sig.inputs.insert(6, address_merkle_tree_root_index_arg); - - // Inject a `LightContext` into the function body. - let light_context_stmt: Stmt = parse_quote! { - let mut ctx: ::light_sdk::context::LightContext< - #accounts_ident, - #light_accounts_ident - > = ::light_sdk::context::LightContext::new( - ctx, - inputs, - merkle_context, - merkle_tree_root_index, - address_merkle_context, - address_merkle_tree_root_index, - )?; - }; - i.block.stmts.insert(0, light_context_stmt); - - // Pack all instruction inputs in a struct, which then can be used in - // `check_constrants` and `derive_address_seeds`. - // - // We do that, because passing one reference to these methods is more - // comfortable. Passing references to each input separately would - // require even messier code... - // - // We move the inputs to that struct, so no copies are being made. - let input_idents = instruction_params - .iter() - .map(|input| input.name.clone()) - .collect::>(); - let inputs_pack_stmt: Stmt = parse_quote! { - let inputs = #inputs_ident { #(#input_idents),* }; - }; - i.block.stmts.insert(1, inputs_pack_stmt); - - // Inject `check_constraints` and `derive_address_seeds` calls right - // after. - let check_constraints_stmt: Stmt = parse_quote! { - ctx.check_constraints(&inputs)?; - }; - i.block.stmts.insert(2, check_constraints_stmt); - let derive_address_seed_stmt: Stmt = parse_quote! { - ctx.derive_address_seeds(address_merkle_context, &inputs); - }; - i.block.stmts.insert(3, derive_address_seed_stmt); - - // Once we are done with calling `check_constraints` and - // `derive_address_seeds`, we can unpack the inputs, so developers can - // use them as regular variables in their code. - // - // Unpacking of the struct means moving the values and no copies are - // being made. - let inputs_unpack_stmt: Stmt = parse_quote! { - let #inputs_ident { #(#input_idents),* } = inputs; - }; - i.block.stmts.insert(4, inputs_unpack_stmt); - - // Inject `verify` statements at the end of the function. - let stmts_len = i.block.stmts.len(); - let verify_stmt: Stmt = parse_quote! { - ctx.verify(proof)?; - }; - i.block.stmts.insert(stmts_len - 1, verify_stmt); - } - - fn visit_item_mod_mut(&mut self, i: &mut ItemMod) { - // Search for all functions inside the annotated `mod` and visit them. - if let Some((_, ref mut content)) = i.content { - for item in content.iter_mut() { - if let Item::Fn(item_fn) = item { - self.visit_item_fn_mut(item_fn) - } - } - } - } -} - -pub(crate) fn program(mut input: ItemMod) -> Result { - let mut transform = LightProgramTransform::default(); - transform.visit_item_mod_mut(&mut input); - - let instruction_params = transform.instruction_params; - - Ok(quote! { - #instruction_params - - #input - }) -} diff --git a/sdk-libs/macros/src/traits.rs b/sdk-libs/macros/src/traits.rs deleted file mode 100644 index 6699ec172b..0000000000 --- a/sdk-libs/macros/src/traits.rs +++ /dev/null @@ -1,401 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; -use syn::{Data, DeriveInput, Fields, FieldsNamed, Ident, Result}; - -pub(crate) fn process_light_traits(input: DeriveInput) -> Result { - let name = &input.ident; - - let trait_impls = match input.data { - Data::Struct(data_struct) => match data_struct.fields { - Fields::Named(fields) => process_fields_and_attributes(name, fields), - _ => quote! { - compile_error!("Error: Expected named fields but found unnamed or no fields."); - }, - }, - _ => quote! {}, - }; - - let expanded = quote! { - #trait_impls - }; - - Ok(expanded) -} - -fn process_fields_and_attributes(name: &Ident, fields: FieldsNamed) -> TokenStream { - let mut self_program_field = None; - let mut fee_payer_field = None; - let mut authority_field = None; - let mut light_system_program_field = None; - let mut cpi_context_account_field = None; - - // base impl - let mut registered_program_pda_field = None; - let mut noop_program_field = None; - let mut account_compression_authority_field = None; - let mut account_compression_program_field = None; - let mut system_program_field = None; - - let compressed_sol_pda_field = fields - .named - .iter() - .find_map(|f| { - if f.ident - .as_ref() - .map(|id| id == "compressed_sol_pda") - .unwrap_or(false) - { - Some(quote! { self.#f.ident.as_ref() }) - } else { - None - } - }) - .unwrap_or(quote! { None }); - - let compression_recipient_field = fields - .named - .iter() - .find_map(|f| { - if f.ident - .as_ref() - .map(|id| id == "compression_recipient") - .unwrap_or(false) - { - Some(quote! { self.#f.ident.as_ref() }) - } else { - None - } - }) - .unwrap_or(quote! { None }); - - for f in fields.named.iter() { - for attr in &f.attrs { - if attr.path().is_ident("self_program") { - self_program_field = Some(f.ident.as_ref().unwrap()); - } - if attr.path().is_ident("fee_payer") { - fee_payer_field = Some(f.ident.as_ref().unwrap()); - } - if attr.path().is_ident("authority") { - authority_field = Some(f.ident.as_ref().unwrap()); - } - if attr.path().is_ident("cpi_context") { - cpi_context_account_field = Some(f.ident.as_ref().unwrap()); - } - } - if f.ident - .as_ref() - .map(|id| id == "light_system_program") - .unwrap_or(false) - { - light_system_program_field = Some(f.ident.as_ref().unwrap()); - } - if f.ident - .as_ref() - .map(|id| id == "registered_program_pda") - .unwrap_or(false) - { - registered_program_pda_field = Some(f.ident.as_ref().unwrap()); - } - if f.ident - .as_ref() - .map(|id| id == "noop_program") - .unwrap_or(false) - { - noop_program_field = Some(f.ident.as_ref().unwrap()); - } - if f.ident - .as_ref() - .map(|id| id == "account_compression_authority") - .unwrap_or(false) - { - account_compression_authority_field = Some(f.ident.as_ref().unwrap()); - } - if f.ident - .as_ref() - .map(|id| id == "account_compression_program") - .unwrap_or(false) - { - account_compression_program_field = Some(f.ident.as_ref().unwrap()); - } - if f.ident - .as_ref() - .map(|id| id == "system_program") - .unwrap_or(false) - { - system_program_field = Some(f.ident.as_ref().unwrap()); - } - } - - // optional: compressed_sol_pda, compression_recipient, cpi_context_account - let missing_required_fields = [ - if light_system_program_field.is_none() { - "light_system_program" - } else { - "" - }, - if registered_program_pda_field.is_none() { - "registered_program_pda" - } else { - "" - }, - if noop_program_field.is_none() { - "noop_program" - } else { - "" - }, - if account_compression_authority_field.is_none() { - "account_compression_authority" - } else { - "" - }, - if account_compression_program_field.is_none() { - "account_compression_program" - } else { - "" - }, - if system_program_field.is_none() { - "system_program" - } else { - "" - }, - ] - .iter() - .filter(|&field| !field.is_empty()) - .cloned() - .collect::>(); - - let missing_required_attributes = [ - if self_program_field.is_none() { - "self_program" - } else { - "" - }, - if fee_payer_field.is_none() { - "fee_payer" - } else { - "" - }, - if authority_field.is_none() { - "authority" - } else { - "" - }, - ] - .iter() - .filter(|&attr| !attr.is_empty()) - .cloned() - .collect::>(); - - if !missing_required_fields.is_empty() || !missing_required_attributes.is_empty() { - let error_message = format!( - "Error: Missing required fields: [{}], Missing required attributes: [{}]", - missing_required_fields.join(", "), - missing_required_attributes.join(", ") - ); - quote! { - compile_error!(#error_message); - } - } else { - let base_impls = quote! { - impl<'info> ::light_sdk::legacy::InvokeCpiAccounts<'info> for #name<'info> { - fn get_invoking_program(&self) -> AccountInfo<'info> { - self.#self_program_field.to_account_info() - } - } - impl<'info> ::light_sdk::legacy::SignerAccounts<'info> for #name<'info> { - fn get_fee_payer(&self) -> ::anchor_lang::prelude::AccountInfo<'info> { - self.#fee_payer_field.to_account_info() - } - fn get_authority(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { - &self.#authority_field - } - } - impl<'info> ::light_sdk::legacy::LightSystemAccount<'info> for #name<'info> { - fn get_light_system_program(&self) -> ::anchor_lang::prelude::AccountInfo<'info> { - self.#light_system_program_field.to_account_info() - } - } - }; - let invoke_accounts_impl = quote! { - impl<'info> ::light_sdk::legacy::InvokeAccounts<'info> for #name<'info> { - fn get_registered_program_pda(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { - &self.#registered_program_pda_field - } - fn get_noop_program(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { - &self.#noop_program_field - } - fn get_account_compression_authority(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { - &self.#account_compression_authority_field - } - fn get_account_compression_program(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { - &self.#account_compression_program_field - } - fn get_system_program(&self) ->::anchor_lang::prelude::AccountInfo<'info> { - self.#system_program_field.to_account_info() - } - fn get_compressed_sol_pda(&self) -> Option<&::anchor_lang::prelude::AccountInfo<'info>> { - #compressed_sol_pda_field - } - fn get_compression_recipient(&self) -> Option<&::anchor_lang::prelude::AccountInfo<'info>> { - #compression_recipient_field - } - } - }; - if cpi_context_account_field.is_none() { - quote! { - #base_impls - #invoke_accounts_impl - impl<'info> ::light_sdk::legacy::InvokeCpiContextAccount<'info> for #name<'info> { - fn get_cpi_context_account(&self) -> Option< - &::anchor_lang::prelude::AccountInfo<'info> - > { - None - } - } - } - } else { - quote! { - #base_impls - #invoke_accounts_impl - impl<'info> ::light_sdk::legacy::InvokeCpiContextAccount<'info> for #name<'info> { - fn get_cpi_context_account(&self) -> Option< - &::anchor_lang::prelude::AccountInfo<'info> - > { - Some(&self.#cpi_context_account_field) - } - } - } - } - } -} - -#[cfg(test)] -mod tests { - use syn::{parse_quote, DeriveInput, FieldsNamed}; - - use super::*; - - #[test] - fn test_process_light_traits() { - let input: DeriveInput = parse_quote! { - struct TestStruct { - #[self_program] - pub my_program: Program<'info, MyProgram>, - #[fee_payer] - pub payer: Signer<'info>, - #[authority] - pub user: AccountInfo<'info>, - pub light_system_program: AccountInfo<'info>, - pub registered_program_pda: AccountInfo<'info>, - pub noop_program: AccountInfo<'info>, - pub account_compression_authority: AccountInfo<'info>, - pub account_compression_program: AccountInfo<'info>, - pub system_program: Program<'info, System>, - } - }; - - let output = process_light_traits(input).unwrap(); - let output_string = output.to_string(); - - assert!(output_string.contains("InvokeCpiAccounts")); - assert!(output_string.contains("SignerAccounts")); - assert!(output_string.contains("LightSystemAccount")); - assert!(output_string.contains("InvokeAccounts")); - assert!(output_string.contains("InvokeCpiContextAccount")); - } - - #[test] - fn test_process_fields_and_attributes() { - let fields: FieldsNamed = parse_quote! { - { - #[self_program] - pub my_program: Program<'info, MyProgram>, - #[fee_payer] - pub payer: Signer<'info>, - #[authority] - pub user: AccountInfo<'info>, - pub light_system_program: AccountInfo<'info>, - pub registered_program_pda: AccountInfo<'info>, - pub noop_program: AccountInfo<'info>, - pub account_compression_authority: AccountInfo<'info>, - pub account_compression_program: AccountInfo<'info>, - pub system_program: Program<'info, System>, - } - }; - - let name = syn::Ident::new("TestStruct", proc_macro2::Span::call_site()); - let output = process_fields_and_attributes(&name, fields); - let output_string = output.to_string(); - - assert!(output_string.contains("InvokeCpiAccounts")); - assert!(output_string.contains("SignerAccounts")); - assert!(output_string.contains("LightSystemAccount")); - assert!(output_string.contains("InvokeAccounts")); - assert!(output_string.contains("InvokeCpiContextAccount")); - } - - #[test] - fn test_process_light_traits_missing_fields() { - let input: DeriveInput = parse_quote! { - struct TestStruct { - #[self_program] - pub my_program: Program<'info, MyProgram>, - #[fee_payer] - pub payer: Signer<'info>, - #[authority] - pub user: AccountInfo<'info>, - // Missing required fields - } - }; - - let result = process_light_traits(input); - let output_string = result.unwrap().to_string(); - - assert!(output_string.contains("compile_error")); - assert!(output_string.contains("Error: Missing required fields: [light_system_program, registered_program_pda, noop_program, account_compression_authority, account_compression_program, system_program], Missing required attributes: []")); - } - - #[test] - fn test_process_light_traits_missing_attributes() { - let input: DeriveInput = parse_quote! { - struct TestStruct { - pub my_program: Program<'info, MyProgram>, // Missing #[self_program] - pub payer: Signer<'info>, // Missing #[fee_payer] - pub user: AccountInfo<'info>, // Missing #[authority] - pub light_system_program: AccountInfo<'info>, - pub registered_program_pda: AccountInfo<'info>, - pub noop_program: AccountInfo<'info>, - pub account_compression_authority: AccountInfo<'info>, - pub account_compression_program: AccountInfo<'info>, - pub system_program: Program<'info, System>, - } - }; - - let result = process_light_traits(input); - let output_string = result.unwrap().to_string(); - assert!(output_string.contains("compile_error")); - assert!(output_string.contains("Error: Missing required fields: [], Missing required attributes: [self_program, fee_payer, authority]")); - } - - #[test] - fn test_process_fields_and_attributes_missing_fields() { - let fields: FieldsNamed = parse_quote! { - { - #[self_program] - pub my_program: Program<'info, MyProgram>, - #[fee_payer] - pub payer: Signer<'info>, - pub user: AccountInfo<'info>, // missing #[authority] - // Missing required fields - } - }; - - let name = syn::Ident::new("TestStruct", proc_macro2::Span::call_site()); - let output = process_fields_and_attributes(&name, fields); - let output_string = output.to_string(); - - assert!(output_string.contains("compile_error")); - assert!(output_string.contains("Error: Missing required fields: [light_system_program, registered_program_pda, noop_program, account_compression_authority, account_compression_program, system_program], Missing required attributes: [authority]")); - } -} diff --git a/sdk-libs/program-test/src/compressible.rs b/sdk-libs/program-test/src/compressible.rs index 92a8461778..9100e8bcd1 100644 --- a/sdk-libs/program-test/src/compressible.rs +++ b/sdk-libs/program-test/src/compressible.rs @@ -42,16 +42,25 @@ fn determine_account_type(data: &[u8]) -> Option { } } +<<<<<<< HEAD /// Extracts CompressionInfo and account type from account data, handling both Token and CMint. /// Returns (CompressionInfo, account_type) or None if parsing fails. #[cfg(feature = "devenv")] fn extract_compression_info(data: &[u8]) -> Option<(CompressionInfo, u8)> { use light_token_interface::state::extensions::ExtensionStruct; +======= +/// Extracts CompressionInfo, account type, and compression_only from account data. +/// Returns (CompressionInfo, account_type, compression_only) or None if parsing fails. +#[cfg(feature = "devenv")] +fn extract_compression_info(data: &[u8]) -> Option<(CompressionInfo, u8, bool)> { + use light_zero_copy::traits::ZeroCopyAt; +>>>>>>> a606eb113 (wip) let account_type = determine_account_type(data)?; match account_type { ACCOUNT_TYPE_TOKEN_ACCOUNT => { +<<<<<<< HEAD let ctoken = Token::deserialize(&mut &data[..]).ok()?; // Get CompressionInfo from Compressible extension let compression_info = @@ -64,10 +73,36 @@ fn extract_compression_info(data: &[u8]) -> Option<(CompressionInfo, u8)> { _ => None, })?; Some((compression_info, account_type)) +======= + let (ctoken, _) = CToken::zero_copy_at(data).ok()?; + let ext = ctoken.get_compressible_extension()?; + + let compression_info = CompressionInfo { + config_account_version: ext.info.config_account_version.into(), + compress_to_pubkey: ext.info.compress_to_pubkey, + account_version: ext.info.account_version, + lamports_per_write: ext.info.lamports_per_write.into(), + compression_authority: ext.info.compression_authority, + rent_sponsor: ext.info.rent_sponsor, + last_claimed_slot: ext.info.last_claimed_slot.into(), + rent_exemption_paid: ext.info.rent_exemption_paid.into(), + _reserved: ext.info._reserved.into(), + rent_config: RentConfig { + base_rent: ext.info.rent_config.base_rent.into(), + compression_cost: ext.info.rent_config.compression_cost.into(), + lamports_per_byte_per_epoch: ext.info.rent_config.lamports_per_byte_per_epoch, + max_funded_epochs: ext.info.rent_config.max_funded_epochs, + max_top_up: ext.info.rent_config.max_top_up.into(), + }, + }; + let compression_only = ext.compression_only != 0; + Some((compression_info, account_type, compression_only)) +>>>>>>> a606eb113 (wip) } ACCOUNT_TYPE_MINT => { let cmint = CompressedMint::deserialize(&mut &data[..]).ok()?; - Some((cmint.compression, account_type)) + // CMint accounts don't have compression_only, default to false + Some((cmint.compression, account_type, false)) } _ => None, } @@ -84,6 +119,8 @@ pub struct StoredCompressibleAccount { pub compression: CompressionInfo, /// Account type: ACCOUNT_TYPE_TOKEN_ACCOUNT (2) or ACCOUNT_TYPE_MINT (1) pub account_type: u8, + /// Whether this is a compression-only account (affects batching) + pub compression_only: bool, } #[cfg(feature = "devenv")] @@ -141,12 +178,20 @@ pub async fn claim_and_compress( .context .get_program_accounts(&light_compressed_token::ID); + // CToken base accounts are 165 bytes, filter above that to exclude empty/minimal accounts for account in compressible_ctoken_accounts .iter() - .filter(|e| e.1.data.len() > 200 && e.1.lamports > 0) + .filter(|e| e.1.data.len() >= 165 && e.1.lamports > 0) { +<<<<<<< HEAD // Extract compression info and account type, handling both Token and CMint let Some((compression, account_type)) = extract_compression_info(&account.1.data) else { +======= + // Extract compression info, account type, and compression_only + let Some((compression, account_type, compression_only)) = + extract_compression_info(&account.1.data) + else { +>>>>>>> a606eb113 (wip) continue; }; @@ -169,13 +214,17 @@ pub async fn claim_and_compress( last_paid_slot: last_funded_slot, compression, account_type, + compression_only, }, ); } let current_slot = rpc.get_slot().await?; - let mut compress_accounts = Vec::new(); + // Separate accounts by type and compression_only setting + let mut compress_accounts_compression_only = Vec::new(); + let mut compress_accounts_normal = Vec::new(); + let mut compress_cmint_accounts = Vec::new(); let mut claim_accounts = Vec::new(); // For each stored account, determine action using AccountRentState @@ -201,10 +250,21 @@ pub async fn claim_and_compress( match state.calculate_claimable_rent(&compression.rent_config, rent_exemption) { None => { // Account is compressible (has rent deficit) +<<<<<<< HEAD // Only Token accounts can be compressed via compress_and_close_forester // CMint accounts have a different compression flow +======= +>>>>>>> a606eb113 (wip) if stored_account.account_type == ACCOUNT_TYPE_TOKEN_ACCOUNT { - compress_accounts.push(*pubkey); + // CToken accounts - separate by compression_only + if stored_account.compression_only { + compress_accounts_compression_only.push(*pubkey); + } else { + compress_accounts_normal.push(*pubkey); + } + } else if stored_account.account_type == ACCOUNT_TYPE_MINT { + // CMint accounts - use mint_action flow + compress_cmint_accounts.push(*pubkey); } } Some(claimable_amount) if claimable_amount > 0 => { @@ -224,17 +284,32 @@ pub async fn claim_and_compress( claim_forester(rpc, token_accounts, &forester_keypair, &payer).await?; } - // Process compressible accounts in batches + // Process compressible accounts in batches, separated by compression_only setting + // This prevents TlvExtensionLengthMismatch errors when batching accounts together const BATCH_SIZE: usize = 10; - for chunk in compress_accounts.chunks(BATCH_SIZE) { + + // Process compression_only=true CToken accounts + for chunk in compress_accounts_compression_only.chunks(BATCH_SIZE) { compress_and_close_forester(rpc, chunk, &forester_keypair, &payer, None).await?; + for account_pubkey in chunk { + stored_compressible_accounts.remove(account_pubkey); + } + } - // Remove compressed accounts from HashMap + // Process compression_only=false CToken accounts + for chunk in compress_accounts_normal.chunks(BATCH_SIZE) { + compress_and_close_forester(rpc, chunk, &forester_keypair, &payer, None).await?; for account_pubkey in chunk { stored_compressible_accounts.remove(account_pubkey); } } + // Process CMint accounts via mint_action + for cmint_pubkey in compress_cmint_accounts { + compress_cmint_forester(rpc, cmint_pubkey, &payer).await?; + stored_compressible_accounts.remove(&cmint_pubkey); + } + Ok(()) } @@ -257,7 +332,7 @@ pub async fn auto_compress_program_pdas( let cfg = CpdaCompressibleConfig::try_from_slice(&cfg_acc.data) .map_err(|e| RpcError::CustomError(format!("config deserialize: {e:?}")))?; let rent_sponsor = cfg.rent_sponsor; - // TODO: add coverage for external compression_authority + // compression_authority is the payer by default for auto-compress let compression_authority = payer.pubkey(); let address_tree = cfg.address_space[0]; @@ -267,11 +342,16 @@ pub async fn auto_compress_program_pdas( return Ok(()); } + // CompressAccountsIdempotent struct expects 4 accounts: + // 1. fee_payer (signer, writable) + // 2. config (read-only) + // 3. rent_sponsor (writable) + // 4. compression_authority (writable - per generated struct) let program_metas = vec![ AccountMeta::new(payer.pubkey(), true), AccountMeta::new_readonly(config_pda, false), AccountMeta::new(rent_sponsor, false), - AccountMeta::new_readonly(compression_authority, false), + AccountMeta::new(compression_authority, false), ]; const BATCH_SIZE: usize = 5; @@ -309,6 +389,7 @@ async fn try_compress_chunk( // Attempt compression per-account idempotently. for (pda, acc) in chunk.iter() { + // v2 address derive using PDA as seed let addr = derive_address( &pda.to_bytes(), &address_tree.to_bytes(), @@ -354,3 +435,110 @@ async fn try_compress_chunk( .await; } } + +/// Compress and close a CMint account via mint_action instruction. +/// CMint uses MintAction::CompressAndCloseCMint flow instead of registry compress_and_close. +#[cfg(feature = "devenv")] +async fn compress_cmint_forester( + rpc: &mut LightProgramTest, + cmint_pubkey: Pubkey, + payer: &solana_sdk::signature::Keypair, +) -> Result<(), RpcError> { + use light_client::indexer::Indexer; + use light_compressed_account::instruction_data::traits::LightInstructionData; + use light_compressible::config::CompressibleConfig; + use light_ctoken_interface::{ + instructions::mint_action::{ + CompressAndCloseCMintAction, CompressedMintWithContext, + MintActionCompressedInstructionData, + }, + CTOKEN_PROGRAM_ID, + }; + use light_ctoken_sdk::compressed_token::mint_action::MintActionMetaConfig; + use solana_sdk::signature::Signer; + + // Get CMint account data + let cmint_account = rpc.get_account(cmint_pubkey).await?.ok_or_else(|| { + RpcError::CustomError(format!("CMint account {} not found", cmint_pubkey)) + })?; + + // Deserialize CMint to get compressed_address and rent_sponsor + let cmint: CompressedMint = + BorshDeserialize::deserialize(&mut cmint_account.data.as_slice()) + .map_err(|e| RpcError::CustomError(format!("Failed to deserialize CMint: {:?}", e)))?; + + let compressed_mint_address = cmint.metadata.compressed_address; + let rent_sponsor = Pubkey::from(cmint.compression.rent_sponsor); + + // Get the compressed mint account from indexer + let compressed_mint_account = rpc + .get_compressed_account(compressed_mint_address, None) + .await? + .value + .ok_or(RpcError::AccountDoesNotExist(format!( + "Compressed mint {:?}", + compressed_mint_address + )))?; + + // Get validity proof + let rpc_proof_result = rpc + .get_validity_proof(vec![compressed_mint_account.hash], vec![], None) + .await? + .value; + + // Build compressed mint inputs + // IMPORTANT: Set mint to None when CMint is decompressed + // This tells on-chain code to read mint data from CMint Solana account + // (not from instruction data which would have stale compression_info) + let compressed_mint_inputs = CompressedMintWithContext { + prove_by_index: rpc_proof_result.accounts[0].root_index.proof_by_index(), + leaf_index: compressed_mint_account.leaf_index, + root_index: rpc_proof_result.accounts[0] + .root_index + .root_index() + .unwrap_or_default(), + address: compressed_mint_address, + mint: None, // CMint is decompressed, data lives in CMint account + }; + + // Build instruction data with CompressAndCloseCMint action + let instruction_data = MintActionCompressedInstructionData::new( + compressed_mint_inputs, + rpc_proof_result.proof.into(), + ) + .with_compress_and_close_cmint(CompressAndCloseCMintAction { idempotent: 1 }); + + // Get state tree info + let state_tree_info = rpc_proof_result.accounts[0].tree_info; + + // Build account metas - authority can be anyone for permissionless CompressAndCloseCMint + let config_address = CompressibleConfig::ctoken_v1_config_pda(); + let meta_config = MintActionMetaConfig::new( + payer.pubkey(), + payer.pubkey(), // authority doesn't matter for CompressAndCloseCMint + state_tree_info.tree, + state_tree_info.queue, + state_tree_info.queue, + ) + .with_compressible_cmint(cmint_pubkey, config_address, rent_sponsor); + + let account_metas = meta_config.to_account_metas(); + + // Serialize instruction data + let data = instruction_data + .data() + .map_err(|e| RpcError::CustomError(format!("Failed to serialize instruction: {:?}", e)))?; + + // Build instruction + let instruction = solana_instruction::Instruction { + program_id: Pubkey::from(CTOKEN_PROGRAM_ID), + accounts: account_metas, + data, + }; + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await?; + + Ok(()) +} diff --git a/sdk-libs/program-test/src/program_test/light_program_test.rs b/sdk-libs/program-test/src/program_test/light_program_test.rs index 08b4680f26..9e16afd5a3 100644 --- a/sdk-libs/program-test/src/program_test/light_program_test.rs +++ b/sdk-libs/program-test/src/program_test/light_program_test.rs @@ -430,6 +430,280 @@ impl LightProgramTest { self.auto_mine_cold_state_programs .retain(|&pid| pid != program_id); } + + /// Fetches ATA interface for a (mint, owner) pair. + /// + /// Checks on-chain first, then compressed state. + /// Always returns `AtaInterface` with `data` populated so clients can + /// access `amount`, `delegate`, etc. regardless of hot/cold state. + /// + /// Fetches raw ATA account interface with Account bytes always present. + /// + /// For hot accounts: actual on-chain bytes. + /// For cold accounts: synthetic SPL Token Account format bytes. + /// + /// Use `parse_token_account_interface()` to extract typed `TokenData`. + /// + /// # Example + /// ```ignore + /// // 1. Fetch raw account interface (async) + /// let account = rpc.get_ata_account_interface(&mint, &owner).await?; + /// + /// // 2. Parse into token account interface (sync) + /// let parsed = parse_token_account_interface(&account)?; + /// + /// // 3. Check if cold and act accordingly + /// if parsed.is_cold { + /// let proof = rpc.get_validity_proof(vec![parsed.hash().unwrap()], vec![], None).await?; + /// let ixs = build_decompress_token_accounts(&[parsed], fee_payer, Some(proof.value))?; + /// } + /// ``` + pub async fn get_ata_account_interface( + &self, + mint: &solana_sdk::pubkey::Pubkey, + owner: &solana_sdk::pubkey::Pubkey, + ) -> Result { + use light_client::indexer::{ + GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, + }; + use light_compressible_client::{ + pack_token_data_to_spl_bytes, AtaAccountInterface, AtaDecompressionContext, + }; + use light_ctoken_sdk::ctoken::derive_ctoken_ata; + use light_sdk::constants::C_TOKEN_PROGRAM_ID; + + let (ata, bump) = derive_ctoken_ata(owner, mint); + + // Check on-chain first + if let Some(account) = self.context.get_account(&ata) { + return Ok(AtaAccountInterface { + pubkey: ata, + account, + is_cold: false, + decompression_context: None, + }); + } + + // Check compressed state + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new(Some( + *mint, + ))); + let result = self + .get_compressed_token_accounts_by_owner(&ata, options, None) + .await?; + + if let Some(compressed) = result.value.items.into_iter().next() { + // Synthesize SPL Token Account bytes from TokenData + let token_data = &compressed.token; + let data = pack_token_data_to_spl_bytes(mint, &ata, token_data).to_vec(); + + // Create synthetic Account + let account = solana_sdk::account::Account { + lamports: 0, // Compressed accounts don't have lamports + data, + owner: C_TOKEN_PROGRAM_ID.into(), + executable: false, + rent_epoch: 0, + }; + + return Ok(AtaAccountInterface { + pubkey: ata, + account, + is_cold: true, + decompression_context: Some(AtaDecompressionContext { + compressed, + wallet_owner: *owner, + mint: *mint, + bump, + }), + }); + } + + // Doesn't exist - return empty synthetic account + let data = vec![0u8; 165]; + let account = solana_sdk::account::Account { + lamports: 0, + data, + owner: C_TOKEN_PROGRAM_ID.into(), + executable: false, + rent_epoch: 0, + }; + + Ok(AtaAccountInterface { + pubkey: ata, + account, + is_cold: false, + decompression_context: None, + }) + } + + /// Legacy: Fetches AtaInterface (unified ATA representation). + /// Prefer `get_ata_account_interface()` + `parse_token_account_interface()` for new code. + pub async fn get_ata_interface( + &self, + mint: &solana_sdk::pubkey::Pubkey, + owner: &solana_sdk::pubkey::Pubkey, + ) -> Result { + use light_client::indexer::{ + GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, + }; + use light_compressible_client::{AtaInterface, DecompressionContext, TokenData}; + use light_ctoken_sdk::{compat::AccountState, ctoken::derive_ctoken_ata}; + + let (ata, bump) = derive_ctoken_ata(owner, mint); + + // Check on-chain first + if let Some(account) = self.context.get_account(&ata) { + use solana_sdk::program_pack::Pack; + let token_data = if account.data.len() >= 165 { + let spl_account = + spl_token_2022::state::Account::unpack(&account.data[..165]).unwrap_or_default(); + TokenData { + mint: spl_account.mint, + owner: spl_account.owner, + amount: spl_account.amount, + delegate: spl_account.delegate.into(), + state: match spl_account.state { + spl_token_2022::state::AccountState::Frozen => AccountState::Frozen, + _ => AccountState::Initialized, + }, + tlv: None, + } + } else { + TokenData { + mint: *mint, + owner: ata, + ..Default::default() + } + }; + + return Ok(AtaInterface { + ata, + owner: *owner, + mint: *mint, + bump, + is_cold: false, + token_data, + raw_account: Some(account), + decompression: None, + }); + } + + // Check compressed state + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new(Some( + *mint, + ))); + let result = self + .get_compressed_token_accounts_by_owner(&ata, options, None) + .await?; + + if let Some(compressed) = result.value.items.into_iter().next() { + let token_data = compressed.token.clone(); + + return Ok(AtaInterface { + ata, + owner: *owner, + mint: *mint, + bump, + is_cold: true, + token_data, + raw_account: None, + decompression: Some(DecompressionContext { compressed }), + }); + } + + // Doesn't exist + Ok(AtaInterface { + ata, + owner: *owner, + mint: *mint, + bump, + is_cold: false, + token_data: TokenData { + mint: *mint, + owner: ata, + ..Default::default() + }, + raw_account: None, + decompression: None, + }) + } + + /// Fetches MintInterface for a mint signer pubkey. + /// + /// Checks on-chain first, then compressed state. + /// Returns `MintInterface` with state: + /// - `Hot` if CMint exists on-chain + /// - `Cold` if CMint is compressed (needs decompression) + /// - `None` if CMint doesn't exist + /// + /// # Example + /// ```ignore + /// let mint = rpc.get_mint_interface(&signer).await?; + /// if mint.is_cold() { + /// // Need to decompress + /// } + /// ``` + pub async fn get_mint_interface( + &self, + signer: &solana_sdk::pubkey::Pubkey, + ) -> Result { + use borsh::BorshDeserialize; + use light_client::indexer::Indexer; + use light_compressible_client::{MintInterface, MintState}; + use light_ctoken_interface::{state::CompressedMint, CMINT_ADDRESS_TREE}; + use light_ctoken_sdk::ctoken::{derive_cmint_compressed_address, find_cmint_address}; + + let (cmint, _) = find_cmint_address(signer); + let address_tree = solana_sdk::pubkey::Pubkey::new_from_array(CMINT_ADDRESS_TREE); + let compressed_address = derive_cmint_compressed_address(signer, &address_tree); + + // Check on-chain first + if let Some(account) = self.context.get_account(&cmint) { + return Ok(MintInterface { + cmint, + signer: *signer, + address_tree, + compressed_address, + state: MintState::Hot { account }, + }); + } + + // Check compressed state + let result = self + .get_compressed_account(compressed_address, None) + .await?; + + if let Some(compressed) = result.value { + // Parse mint data if available + if let Some(data) = compressed.data.as_ref() { + if !data.data.is_empty() { + if let Ok(mint_data) = CompressedMint::try_from_slice(&data.data) { + return Ok(MintInterface { + cmint, + signer: *signer, + address_tree, + compressed_address, + state: MintState::Cold { + compressed, + mint_data, + }, + }); + } + } + } + // Empty data = already decompressed (return None, not Cold) + } + + // Doesn't exist + Ok(MintInterface { + cmint, + signer: *signer, + address_tree, + compressed_address, + state: MintState::None, + }) + } } impl MerkleTreeExt for LightProgramTest {} diff --git a/sdk-libs/sdk/src/compressible/compress_account.rs b/sdk-libs/sdk/src/compressible/compress_account.rs index 77e7c46a6a..20ded890cc 100644 --- a/sdk-libs/sdk/src/compressible/compress_account.rs +++ b/sdk-libs/sdk/src/compressible/compress_account.rs @@ -53,6 +53,7 @@ where { use light_compressed_account::address::derive_address; + // v2 address derive using PDA as seed let derived_c_pda = derive_address( &account_info.key.to_bytes(), &address_space[0].to_bytes(), 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 e5af904bc9..24c46eb102 100644 --- a/sdk-libs/sdk/src/compressible/compress_account_on_init.rs +++ b/sdk-libs/sdk/src/compressible/compress_account_on_init.rs @@ -52,6 +52,13 @@ where + Clone + HasCompressionInfo, { + // TODO: consider not supporting yet. + // Fail-fast: with_data=true is not yet supported in macro-generated code + // if with_data { + // msg!("with_data=true is not supported yet"); + // return Err(LightSdkError::ConstraintViolation.into()); + // } + let tree = cpi_accounts .get_tree_account_info(new_address_param.address_merkle_tree_account_index as usize) .map_err(|_| { diff --git a/sdk-libs/sdk/src/compressible/decompress_runtime.rs b/sdk-libs/sdk/src/compressible/decompress_runtime.rs index 10ece4b6ae..1a19ba9480 100644 --- a/sdk-libs/sdk/src/compressible/decompress_runtime.rs +++ b/sdk-libs/sdk/src/compressible/decompress_runtime.rs @@ -1,6 +1,5 @@ //! Traits and processor for decompress_accounts_idempotent instruction. use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo; -#[cfg(feature = "cpi-context")] use light_sdk_types::cpi_context_write::CpiContextWriteAccounts; use light_sdk_types::{ cpi_accounts::CpiAccountsConfig, @@ -19,7 +18,7 @@ use crate::{ AnchorDeserialize, AnchorSerialize, LightDiscriminator, }; -/// Trait for account variants that can be checked for token vs PDA type. +/// Trait for account variants that can be checked for token or PDA type. pub trait HasTokenVariant { /// Returns true if this variant represents a token account (PackedTokenData). fn is_packed_token(&self) -> bool; @@ -27,24 +26,22 @@ pub trait HasTokenVariant { /// Trait for token seed providers. /// +<<<<<<< HEAD /// Also defined in compressed-token-sdk for token-specific runtime helpers. pub trait TokenSeedProvider: Copy { /// Type of accounts struct needed for seed derivation. type Accounts<'info>; +======= +/// After Phase 8 refactor: The variant itself contains resolved seed pubkeys, +/// so no accounts struct is needed for seed derivation. +pub trait CTokenSeedProvider: Copy { +>>>>>>> a606eb113 (wip) /// Get seeds for the token account PDA (used for decompression). - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [AccountInfo<'info>], - ) -> Result<(Vec>, Pubkey), ProgramError>; + fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; /// Get authority seeds for signing during compression. - fn get_authority_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [AccountInfo<'info>], - ) -> Result<(Vec>, Pubkey), ProgramError>; + fn get_authority_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; } /// Context trait for decompression. @@ -71,8 +68,6 @@ pub trait DecompressContext<'info> { fn token_config(&self) -> Option<&AccountInfo<'info>>; /// Collect and unpack compressed accounts into PDAs and tokens. - /// - /// Caller program-specific: handles variant matching and PDA seed derivation. #[allow(clippy::type_complexity)] #[allow(clippy::too_many_arguments)] fn collect_pda_and_token<'b>( @@ -88,8 +83,6 @@ pub trait DecompressContext<'info> { ), ProgramError>; /// Process token decompression. - /// - /// Caller program-specific: handles token account creation and seed derivation. #[allow(clippy::too_many_arguments)] fn process_tokens<'b>( &self, @@ -104,20 +97,11 @@ pub trait DecompressContext<'info> { proof: crate::instruction::ValidityProof, cpi_accounts: &CpiAccounts<'b, 'info>, post_system_accounts: &[AccountInfo<'info>], - has_pdas: bool, + has_prior_context: bool, ) -> Result<(), ProgramError>; } /// Trait for PDA types that can derive seeds with full account context access. -/// -/// - A: The accounts struct type (typically DecompressAccountsIdempotent<'info>) -/// - S: The SeedParams struct containing data.* field values from instruction data -/// -/// This allows PDA seeds to reference: -/// - `data.*` fields from instruction parameters (seed_params.field) -/// - `ctx.*` accounts from the instruction context (accounts.field) -/// -/// For off-chain PDA derivation, use the generated client helper functions (get_*_seeds). pub trait PdaSeedDerivation { fn derive_pda_seeds_with_accounts( &self, @@ -127,7 +111,8 @@ pub trait PdaSeedDerivation { ) -> Result<(Vec>, Pubkey), ProgramError>; } -/// Check compressed accounts to determine if we have tokens and/or PDAs. +/// Check what types of accounts are in the batch. +/// Returns (has_tokens, has_pdas). #[inline(never)] pub fn check_account_types(compressed_accounts: &[T]) -> (bool, bool) { let (mut has_tokens, mut has_pdas) = (false, false); @@ -176,9 +161,6 @@ where { let data: T = P::unpack(packed, post_system_accounts)?; - // CHECK: pda match - // Call the method with account context and seed params - // Note: Some implementations may use S::default() when seed_params is None for static seeds let (seeds_vec, derived_pda) = if let Some(params) = seed_params { data.derive_pda_seeds_with_accounts(program_id, seed_accounts, params)? } else { @@ -198,7 +180,6 @@ where )); } - // prepare decompression let compressed_infos = { let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v| v.as_slice()).collect(); crate::compressible::decompress_idempotent::prepare_account_for_decompression_idempotent::( @@ -221,6 +202,10 @@ where } /// Processor for decompress_accounts_idempotent. +/// +/// CPI context batching rules: +/// - Can use inputs from N trees +/// - All inputs must use the FIRST CPI context account of the FIRST input #[inline(never)] #[allow(clippy::too_many_arguments)] pub fn process_decompress_accounts_idempotent<'info, Ctx>( @@ -250,7 +235,10 @@ where return Err(ProgramError::NotEnoughAccountKeys); } - let cpi_accounts = if has_tokens { + // Use CPI context batching when we have both PDAs and tokens + // CPI context can handle inputs from N trees - all use FIRST cpi context of FIRST input + let needs_cpi_context = has_tokens && has_pdas; + let cpi_accounts = if needs_cpi_context { CpiAccounts::new_with_config( ctx.fee_payer(), &remaining_accounts[system_accounts_offset_usize..], @@ -271,13 +259,7 @@ where let solana_accounts = remaining_accounts .get(pda_accounts_start..) .ok_or_else(|| ProgramError::from(crate::error::LightSdkError::ConstraintViolation))?; - let post_system_offset = cpi_accounts.system_accounts_end_offset(); - let all_infos = cpi_accounts.account_infos(); - let post_system_accounts = all_infos - .get(post_system_offset..) - .ok_or_else(|| ProgramError::from(crate::error::LightSdkError::ConstraintViolation))?; - // Call trait method for program-specific collection let (compressed_pda_infos, compressed_token_accounts) = ctx.collect_pda_and_token( &cpi_accounts, address_space, @@ -288,50 +270,57 @@ where let has_pdas = !compressed_pda_infos.is_empty(); let has_tokens = !compressed_token_accounts.is_empty(); + if !has_pdas && !has_tokens { return Ok(()); } let fee_payer = ctx.fee_payer(); - // Decompress PDAs via LightSystemProgram - #[cfg(feature = "cpi-context")] - if has_pdas && has_tokens { - let authority = cpi_accounts - .authority() - .map_err(|_| ProgramError::MissingRequiredSignature)?; - let cpi_context = cpi_accounts - .cpi_context() - .map_err(|_| ProgramError::MissingRequiredSignature)?; - let system_cpi_accounts = CpiContextWriteAccounts { - fee_payer, - authority, - cpi_context, - cpi_signer, - }; - - LightSystemProgramCpi::new_cpi(cpi_signer, proof) - .with_account_infos(&compressed_pda_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(system_cpi_accounts)?; - } else if has_pdas { - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_account_infos(&compressed_pda_infos) - .invoke(cpi_accounts.clone())?; - } - - // TODO: fix this - #[cfg(not(feature = "cpi-context"))] + // Process PDAs (if any) if has_pdas { - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_account_infos(&compressed_pda_infos) - .invoke(cpi_accounts.clone())?; + if !has_tokens { + // PDAs only - execute directly + LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) + .with_account_infos(&compressed_pda_infos) + .invoke(cpi_accounts.clone())?; + } else { + // PDAs + tokens - write to CPI context first, tokens will execute + let authority = cpi_accounts + .authority() + .map_err(|_| ProgramError::MissingRequiredSignature)?; + let cpi_context = cpi_accounts + .cpi_context() + .map_err(|_| ProgramError::MissingRequiredSignature)?; + let system_cpi_accounts = CpiContextWriteAccounts { + fee_payer, + authority, + cpi_context, + cpi_signer, + }; + + LightSystemProgramCpi::new_cpi(cpi_signer, proof) + .with_account_infos(&compressed_pda_infos) + .write_to_cpi_context_first() + .invoke_write_to_cpi_context_first(system_cpi_accounts)?; + } } - // Decompress tokens via trait method + // Process tokens (if any) - executes and consumes CPI context if PDAs wrote to it if has_tokens { +<<<<<<< HEAD let token_program = ctx .token_program() +======= + let post_system_offset = cpi_accounts.system_accounts_end_offset(); + let all_infos = cpi_accounts.account_infos(); + let post_system_accounts = all_infos + .get(post_system_offset..) + .ok_or_else(|| ProgramError::from(crate::error::LightSdkError::ConstraintViolation))?; + + let ctoken_program = ctx + .ctoken_program() +>>>>>>> a606eb113 (wip) .ok_or(ProgramError::NotEnoughAccountKeys)?; let token_rent_sponsor = ctx .token_rent_sponsor() @@ -355,7 +344,7 @@ where proof, &cpi_accounts, post_system_accounts, - has_pdas, + has_pdas, // has_prior_context: PDAs wrote to CPI context )?; } diff --git a/sdk-libs/sdk/src/compressible/finalize.rs b/sdk-libs/sdk/src/compressible/finalize.rs new file mode 100644 index 0000000000..52bec99caf --- /dev/null +++ b/sdk-libs/sdk/src/compressible/finalize.rs @@ -0,0 +1,105 @@ +//! LightFinalize and LightPreInit traits for compression operations. +//! +//! These traits are implemented by the `#[derive(LightFinalize)]` macro from light-sdk-macros. +//! They provide hooks for running compression operations at different points in an instruction: +//! +//! - `LightPreInit`: Called at START of instruction - creates mints via CPI context write +//! - `LightFinalize`: Called at END of instruction - compresses PDAs and executes with proof +//! +//! This two-phase design allows mints to be created BEFORE the instruction body runs, +//! so they can be used during the instruction (e.g., for vault creation, minting tokens). + +use solana_account_info::AccountInfo; + +/// Trait for pre-initialization operations (mint creation). +/// +/// This is generated by `#[derive(LightFinalize)]` when `#[light_mint]` fields exist. +/// Called at the START of an instruction to write mint creation to CPI context. +/// +/// The mints are written to CPI context but NOT executed yet - execution happens +/// in `light_finalize()` at the end, allowing the shared proof to cover both +/// mints and PDAs. +/// +/// # Type Parameters +/// * `'info` - The account info lifetime +/// * `P` - The instruction params type (from `#[instruction(params: P)]`) +pub trait LightPreInit<'info, P> { + /// Execute pre-initialization operations (mint creation). + /// + /// This writes mint creation operations to CPI context. The actual execution + /// with proof happens in `light_finalize()`. + /// + /// # Arguments + /// * `remaining_accounts` - The remaining accounts from the context, used for CPI + /// * `params` - The instruction parameters containing compression data + /// + /// # Returns + /// `true` if mints were written to CPI context and `light_finalize` should execute + /// with CPI context. `false` if no mints exist and normal flow should proceed. + fn light_pre_init( + &mut self, + remaining_accounts: &[AccountInfo<'info>], + params: &P, + ) -> Result; +} + +/// Trait for finalizing compression operations on accounts. +/// +/// This is generated by `#[derive(LightFinalize)]` from light-sdk-macros. +/// Use with `#[light_instruction]` attribute for automatic invocation. +/// +/// # Type Parameters +/// * `'info` - The account info lifetime +/// * `P` - The instruction params type (from `#[instruction(params: P)]`) +/// +/// # Example +/// +/// ```ignore +/// use anchor_lang::prelude::*; +/// use light_sdk::compressible::LightFinalize; +/// use light_sdk_macros::{LightFinalize, light_instruction}; +/// +/// #[derive(Accounts, LightFinalize)] +/// #[instruction(params: CompressionParams)] +/// pub struct CreateCompressible<'info> { +/// #[account(mut)] +/// pub fee_payer: Signer<'info>, +/// +/// #[account(mut)] +/// #[compressible( +/// address_tree_info = params.address_tree_info, +/// output_tree = 0 +/// )] +/// pub my_account: Account<'info, MyData>, +/// +/// /// CHECK: Compression config +/// pub compression_config: AccountInfo<'info>, +/// } +/// +/// // Auto-calls light_pre_init at start, light_finalize at end +/// #[light_instruction(params)] +/// pub fn create_compressible(ctx: Context, params: CompressionParams) -> Result<()> { +/// ctx.accounts.my_account.value = params.value; +/// Ok(()) +/// } +/// ``` +pub trait LightFinalize<'info, P> { + /// Execute compression finalization. + /// + /// This method is called at the end of an instruction to batch and execute + /// all compression CPIs for accounts marked with `#[compressible(...)]`. + /// + /// # Arguments + /// * `remaining_accounts` - The remaining accounts from the context, used for CPI + /// * `params` - The instruction parameters containing compression data + /// * `has_pre_init` - Whether `light_pre_init` was called and wrote to CPI context + /// + /// # Errors + /// Returns an error if the compression CPI fails. + fn light_finalize( + &mut self, + remaining_accounts: &[AccountInfo<'info>], + params: &P, + has_pre_init: bool, + ) -> Result<(), crate::error::LightSdkError>; +} diff --git a/sdk-libs/sdk/src/compressible/mod.rs b/sdk-libs/sdk/src/compressible/mod.rs index 666751140d..a9e0ab9392 100644 --- a/sdk-libs/sdk/src/compressible/mod.rs +++ b/sdk-libs/sdk/src/compressible/mod.rs @@ -1,6 +1,11 @@ pub mod close; pub mod compression_info; pub mod config; +pub mod finalize; +pub mod traits; + +pub use finalize::{LightFinalize, LightPreInit}; +pub use traits::{IntoCTokenVariant, IntoVariant}; #[cfg(feature = "v2")] pub mod compress_account; @@ -10,7 +15,7 @@ pub mod compress_account_on_init; pub mod compress_runtime; #[cfg(feature = "v2")] pub mod decompress_idempotent; -#[cfg(feature = "v2")] +#[cfg(all(feature = "v2", feature = "cpi-context"))] pub mod decompress_runtime; #[cfg(feature = "v2")] pub use close::close; @@ -33,7 +38,7 @@ pub use config::{ pub use decompress_idempotent::{ into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, }; -#[cfg(feature = "v2")] +#[cfg(all(feature = "v2", feature = "cpi-context"))] pub use decompress_runtime::{ check_account_types, handle_packed_pda_variant, process_decompress_accounts_idempotent, DecompressContext, HasTokenVariant, PdaSeedDerivation, TokenSeedProvider, diff --git a/sdk-libs/sdk/src/compressible/traits.rs b/sdk-libs/sdk/src/compressible/traits.rs new file mode 100644 index 0000000000..901a28de41 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/traits.rs @@ -0,0 +1,64 @@ +//! Traits for decompression variant construction. +//! +//! These traits enable ergonomic client-side construction of `RentFreeDecompressAccount` +//! from seeds and compressed account data. + +#[cfg(feature = "anchor")] +use anchor_lang::error::Error; +#[cfg(not(feature = "anchor"))] +use solana_program_error::ProgramError as Error; + +/// Trait for seeds that can construct a compressed account variant. +/// +/// Implemented by generated `XxxSeeds` structs (e.g., `UserRecordSeeds`). +/// The macro generates impls that deserialize account data and verify seeds match. +/// +/// # Example (generated code) +/// ```ignore +/// impl IntoVariant for UserRecordSeeds { +/// fn into_variant(self, data: &[u8]) -> Result { +/// RentFreeAccountVariant::user_record(data, self) +/// } +/// } +/// ``` +pub trait IntoVariant { + /// Construct variant from compressed account data bytes and these seeds. + /// + /// # Arguments + /// * `data` - Raw compressed account data bytes + /// + /// # Returns + /// The constructed variant on success, or an error if: + /// - Deserialization fails + /// - Seed verification fails (data.* seeds don't match account data) + fn into_variant(self, data: &[u8]) -> Result; +} + +/// Trait for CToken account variant types that can construct a full variant with token data. +/// +/// Implemented by generated `CTokenAccountVariant` enum. +/// The macro generates the impl that wraps variant + token_data into `RentFreeAccountVariant`. +/// +/// # Example (generated code) +/// ```ignore +/// impl IntoCTokenVariant for CTokenAccountVariant { +/// fn into_ctoken_variant(self, token_data: TokenData) -> RentFreeAccountVariant { +/// RentFreeAccountVariant::CTokenData(CTokenData { +/// variant: self, +/// token_data, +/// }) +/// } +/// } +/// ``` +/// +/// Type parameter `T` is typically `light_ctoken_sdk::compat::TokenData`. +pub trait IntoCTokenVariant { + /// Construct variant from CToken variant and token data. + /// + /// # Arguments + /// * `token_data` - The parsed `TokenData` from compressed account bytes + /// + /// # Returns + /// The constructed variant containing both CToken variant and token data + fn into_ctoken_variant(self, token_data: T) -> V; +} diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index c673edbdec..cc6369988b 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -174,8 +174,8 @@ pub use light_hasher; use light_hasher::DataHasher; pub use light_macros::{derive_light_cpi_signer, derive_light_cpi_signer_pda}; pub use light_sdk_macros::{ - derive_light_rent_sponsor, derive_light_rent_sponsor_pda, light_system_accounts, - LightDiscriminator, LightHasher, LightHasherSha, LightTraits, + derive_light_rent_sponsor, derive_light_rent_sponsor_pda, LightDiscriminator, LightHasher, + LightHasherSha, }; pub use light_sdk_types::{constants, CpiSigner}; use solana_account_info::AccountInfo; diff --git a/sdk-libs/token-sdk/Cargo.toml b/sdk-libs/token-sdk/Cargo.toml index 5abd8a7cbd..11b1f61435 100644 --- a/sdk-libs/token-sdk/Cargo.toml +++ b/sdk-libs/token-sdk/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/Lightprotocol/light-protocol" [features] default = [] v1 = [] -compressible = [] +compressible = ["cpi-context"] anchor = ["anchor-lang", "light-token-types/anchor", "light-token-interface/anchor"] cpi-context = ["light-sdk/cpi-context"] diff --git a/sdk-libs/token-sdk/src/compressed_token/v2/decompress_full.rs b/sdk-libs/token-sdk/src/compressed_token/v2/decompress_full.rs index 2e217b8f97..81829891c6 100644 --- a/sdk-libs/token-sdk/src/compressed_token/v2/decompress_full.rs +++ b/sdk-libs/token-sdk/src/compressed_token/v2/decompress_full.rs @@ -33,6 +33,9 @@ pub struct DecompressFullIndices { /// TLV extensions for this compressed account (e.g., CompressedOnly extension). /// Used to transfer extension state during decompress. pub tlv: Option>, + /// Whether this is an ATA decompression. For ATAs, the source.owner is the ATA address + /// (not the wallet), so it should NOT be marked as a signer - the wallet signs the tx instead. + pub is_ata: bool, } /// Decompress full balance from compressed token accounts with pre-computed indices @@ -90,7 +93,11 @@ pub fn decompress_full_token_accounts_with_indices<'info>( if owner_idx >= signer_flags.len() { return Err(TokenSdkError::InvalidAccountData); } - signer_flags[owner_idx] = true; + // For ATAs, the owner is the ATA address (a PDA that can't sign). + // The wallet signs the transaction instead, so don't mark the owner as signer. + if !idx.is_ata { + signer_flags[owner_idx] = true; + } } let mut packed_account_metas = Vec::with_capacity(packed_accounts.len()); @@ -187,6 +194,7 @@ pub fn pack_for_decompress_full( source, destination_index: packed_accounts.insert_or_get(destination), tlv, + is_ata: false, // Non-ATA: owner is a signer } } @@ -235,6 +243,7 @@ pub fn pack_for_decompress_full_with_ata( source, destination_index: packed_accounts.insert_or_get(destination), tlv, + is_ata, } } diff --git a/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs b/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs index e87be187c2..f9916ffdd6 100644 --- a/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs +++ b/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs @@ -10,7 +10,9 @@ use solana_program_error::ProgramError; use solana_pubkey::Pubkey; use crate::compat::PackedCTokenData; +use crate::pack::Unpack; +<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs /// Trait for getting token account seeds. pub trait TokenSeedProvider: Copy { /// Type of accounts struct needed for seed derivation. @@ -32,12 +34,26 @@ pub trait TokenSeedProvider: Copy { remaining_accounts: &'a [AccountInfo<'info>], ) -> Result<(Vec>, Pubkey), ProgramError>; } +======= +// Re-export CTokenSeedProvider from sdk (canonical definition). +pub use light_sdk::compressible::CTokenSeedProvider; +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs /// Token decompression processor. +/// +/// Handles both program-owned tokens and ATAs in unified flow. +/// - Program-owned tokens: program signs via CPI with seeds +/// - ATAs: wallet owner signs on transaction (no program signing needed) +/// +/// CPI context usage: +/// - has_prior_context=true: PDAs/Mints already wrote to CPI context, tokens CONSUME it +/// - has_prior_context=false: tokens-only flow, no CPI context needed +/// +/// After Phase 8 refactor: V is `PackedCTokenAccountVariant` which unpacks to +/// `CTokenAccountVariant` containing resolved seed Pubkeys. No accounts struct needed. #[inline(never)] #[allow(clippy::too_many_arguments)] -pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( - accounts_for_seeds: &A, +pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V>( remaining_accounts: &[AccountInfo<'info>], fee_payer: &AccountInfo<'info>, token_program: &AccountInfo<'info>, @@ -52,30 +68,47 @@ pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( proof: ValidityProof, cpi_accounts: &CpiAccounts<'b, 'info>, post_system_accounts: &[AccountInfo<'info>], - has_pdas: bool, + has_prior_context: bool, program_id: &Pubkey, ) -> Result<(), ProgramError> where +<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs V: TokenSeedProvider = A>, A: 'info, +======= + V: Unpack + Copy, + V::Unpacked: CTokenSeedProvider, +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs { + if ctoken_accounts.is_empty() { + return Ok(()); + } + let mut token_decompress_indices: Vec< crate::compressed_token::decompress_full::DecompressFullIndices, +<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs > = Vec::with_capacity(token_accounts.len()); let mut token_signers_seed_groups: Vec>> = Vec::with_capacity(token_accounts.len()); +======= + > = Vec::with_capacity(ctoken_accounts.len()); + // Only program-owned tokens need signer seeds + let mut token_signers_seed_groups: Vec>> = + Vec::with_capacity(ctoken_accounts.len()); +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs let packed_accounts = post_system_accounts; - let authority = cpi_accounts - .authority() - .map_err(|_| ProgramError::MissingRequiredSignature)?; - let cpi_context_pubkey = if has_pdas { - Some( - *cpi_accounts - .cpi_context() - .map_err(|_| ProgramError::MissingRequiredSignature)? - .key, - ) + // CPI context usage for token decompression: + // - If has_prior_context: PDAs/Mints already wrote to CPI context, tokens CONSUME it + // - If !has_prior_context: tokens-only flow, execute directly without CPI context + // + // Note: CPI context supports cross-tree batching. Writes from different trees + // are stored without validation. The only constraint is the executor's first + // input/output must match the CPI context account's associated_merkle_tree. + let cpi_context_pubkey = if has_prior_context { + // PDAs/Mints wrote to context, tokens consume it + cpi_accounts.cpi_context().ok().map(|ctx| *ctx.key) } else { + // Tokens-only: execute directly without CPI context None }; @@ -105,10 +138,19 @@ where } let owner_info = &packed_accounts[owner_index_usize]; +<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs // Use trait method to get seeds (program-specific) let (token_signer_seeds, derived_token_account_address) = token_data .variant .get_seeds(accounts_for_seeds, remaining_accounts)?; +======= + // Unpack the variant to get resolved seed Pubkeys + let unpacked_variant = token_data.variant.unpack(post_system_accounts)?; + + // Program-owned token: use program-derived seeds + let (ctoken_signer_seeds, derived_token_account_address) = + unpacked_variant.get_seeds(program_id)?; +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs if derived_token_account_address != *owner_info.key { msg!( @@ -119,11 +161,24 @@ where return Err(ProgramError::InvalidAccountData); } +<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs let seed_refs: Vec<&[u8]> = token_signer_seeds.iter().map(|s| s.as_slice()).collect(); let seeds_slice: &[&[u8]] = &seed_refs; // Build CompressToPubkey from the signer seeds if bump is present let compress_to_pubkey = token_signer_seeds +======= + // Derive the authority PDA that will own this CToken account (like cp-swap's vault_authority) + let (_authority_seeds, derived_authority_pda) = + unpacked_variant.get_authority_seeds(program_id)?; + + let seed_refs: Vec<&[u8]> = ctoken_signer_seeds.iter().map(|s| s.as_slice()).collect(); + let seeds_slice: &[&[u8]] = &seed_refs; + + // Build CompressToPubkey from the token account seeds + // This ensures compressed TokenData.owner = token account address (not authority) + let compress_to_pubkey = ctoken_signer_seeds +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs .last() .and_then(|b| b.first().copied()) .map(|bump| { @@ -143,10 +198,19 @@ where payer: fee_payer.clone(), account: (*owner_info).clone(), mint: (*mint_info).clone(), +<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs owner: *authority.key, compressible: crate::token::CompressibleParamsCpi { compressible_config: token_config.clone(), rent_sponsor: token_rent_sponsor.clone(), +======= + owner: derived_authority_pda, // Use derived authority PDA (like cp-swap's vault_authority) + } + .invoke_signed_with( + crate::ctoken::CompressibleParamsCpi { + compressible_config: ctoken_config.clone(), + rent_sponsor: ctoken_rent_sponsor.clone(), +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs system_program: cpi_accounts .system_program() .map_err(|_| ProgramError::InvalidAccountData)? @@ -157,8 +221,8 @@ where token_account_version: light_token_interface::state::TokenDataVersion::ShaFlat, compression_only: false, }, - } - .invoke_signed(&[seeds_slice])?; + &[seeds_slice], + )?; let source = MultiInputTokenDataWithContext { owner: token_data.token_data.owner, @@ -174,13 +238,23 @@ where source, destination_index: owner_index, tlv: None, + is_ata: false, // Program-owned token: owner is a signer (via CPI seeds) }; token_decompress_indices.push(decompress_index); token_signers_seed_groups.push(token_signer_seeds); } +<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs let token_ix = crate::compressed_token::decompress_full::decompress_full_token_accounts_with_indices( +======= + if token_decompress_indices.is_empty() { + return Ok(()); + } + + let ctoken_ix = + crate::compressed_token::decompress_full::decompress_full_ctoken_accounts_with_indices( +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs *fee_payer.key, proof, cpi_context_pubkey, @@ -189,26 +263,86 @@ where ) .map_err(ProgramError::from)?; + // Build account infos for CPI. Must include all accounts needed by the transfer2 instruction: + // - System accounts (light_system_program, registered_program_pda, etc.) + // - Fee payer, ctoken accounts + // - CPI context (if present) + // - All packed accounts (post_system_accounts) let mut all_account_infos: Vec> = - Vec::with_capacity(1 + post_system_accounts.len() + 3); + Vec::with_capacity(12 + post_system_accounts.len()); all_account_infos.push(fee_payer.clone()); all_account_infos.push(token_cpi_authority.clone()); all_account_infos.push(token_program.clone()); all_account_infos.push(token_rent_sponsor.clone()); all_account_infos.push(config.clone()); + + // Add required system accounts for transfer2 instruction + // Light system program is at index 0 in the cpi_accounts slice + all_account_infos.push( + cpi_accounts + .account_infos() + .first() + .ok_or(ProgramError::NotEnoughAccountKeys)? + .clone(), + ); + all_account_infos.push( + cpi_accounts + .registered_program_pda() + .map_err(|_| ProgramError::InvalidAccountData)? + .clone(), + ); + all_account_infos.push( + cpi_accounts + .account_compression_authority() + .map_err(|_| ProgramError::InvalidAccountData)? + .clone(), + ); + all_account_infos.push( + cpi_accounts + .account_compression_program() + .map_err(|_| ProgramError::InvalidAccountData)? + .clone(), + ); + all_account_infos.push( + cpi_accounts + .system_program() + .map_err(|_| ProgramError::InvalidAccountData)? + .clone(), + ); + + // Add CPI context if present + if let Ok(cpi_context) = cpi_accounts.cpi_context() { + all_account_infos.push(cpi_context.clone()); + } + all_account_infos.extend_from_slice(post_system_accounts); - let signer_seed_refs: Vec> = token_signers_seed_groups - .iter() - .map(|group| group.iter().map(|s| s.as_slice()).collect()) - .collect(); - let signer_seed_slices: Vec<&[&[u8]]> = signer_seed_refs.iter().map(|g| g.as_slice()).collect(); + // Only include signer seeds for program-owned tokens + if token_signers_seed_groups.is_empty() { + // All tokens were ATAs - no program signing needed + solana_cpi::invoke(&ctoken_ix, all_account_infos.as_slice())?; + } else { + let signer_seed_refs: Vec> = token_signers_seed_groups + .iter() + .map(|group| group.iter().map(|s| s.as_slice()).collect()) + .collect(); + let signer_seed_slices: Vec<&[&[u8]]> = + signer_seed_refs.iter().map(|g| g.as_slice()).collect(); +<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs solana_cpi::invoke_signed( &token_ix, all_account_infos.as_slice(), signer_seed_slices.as_slice(), )?; +======= + solana_cpi::invoke_signed( + &ctoken_ix, + all_account_infos.as_slice(), + signer_seed_slices.as_slice(), + )?; + } +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs Ok(()) } diff --git a/sdk-libs/token-sdk/src/pack.rs b/sdk-libs/token-sdk/src/pack.rs index b220ef3c4b..bd79805e5d 100644 --- a/sdk-libs/token-sdk/src/pack.rs +++ b/sdk-libs/token-sdk/src/pack.rs @@ -1,5 +1,9 @@ //! Pack implementation for TokenData types for c-tokens. use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; +<<<<<<< HEAD:sdk-libs/token-sdk/src/pack.rs +======= +use light_ctoken_interface::state::TokenDataVersion; +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/pack.rs use light_sdk::{ instruction::PackedAccounts, light_hasher::{sha256::Sha256BE, HasherError}, @@ -11,7 +15,9 @@ use solana_program_error::ProgramError; use crate::{AnchorDeserialize, AnchorSerialize}; -// We define the traits here to circumvent the orphan rule. +// Note: We define Pack/Unpack traits locally to circumvent the orphan rule. +// This allows implementing them for external types like TokenData from ctoken-interface. +// The sdk has identical trait definitions in light_sdk::compressible. pub trait Pack { type Packed; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; @@ -24,6 +30,7 @@ pub trait Unpack { ) -> std::result::Result; } +<<<<<<< HEAD:sdk-libs/token-sdk/src/pack.rs impl Pack for TokenData { type Packed = light_token_interface::instructions::transfer2::MultiTokenTransferOutputData; @@ -54,6 +61,8 @@ impl Unpack for TokenData { } } +======= +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/pack.rs /// Solana-compatible token types using `solana_pubkey::Pubkey` pub mod compat { use solana_pubkey::Pubkey; @@ -144,7 +153,7 @@ pub mod compat { } } - impl From for crate::pack::TokenData { + impl From for light_ctoken_interface::state::TokenData { fn from(data: TokenData) -> Self { use light_token_interface::state::CompressedTokenAccountState; @@ -162,8 +171,8 @@ pub mod compat { } } - impl From for TokenData { - fn from(data: crate::pack::TokenData) -> Self { + impl From for TokenData { + fn from(data: light_ctoken_interface::state::TokenData) -> Self { Self { mint: Pubkey::new_from_array(data.mint.to_bytes()), owner: Pubkey::new_from_array(data.owner.to_bytes()), @@ -259,13 +268,14 @@ pub mod compat { impl Pack for CTokenDataWithVariant where - V: AnchorSerialize + Clone + std::fmt::Debug, + V: Pack, + V::Packed: AnchorSerialize + Clone + std::fmt::Debug, { - type Packed = PackedCTokenDataWithVariant; + type Packed = PackedCTokenDataWithVariant; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { PackedCTokenDataWithVariant { - variant: self.variant.clone(), + variant: self.variant.pack(remaining_accounts), token_data: self.token_data.pack(remaining_accounts), } } @@ -281,6 +291,8 @@ pub mod compat { &self, remaining_accounts: &[AccountInfo], ) -> std::result::Result { + // Note: This impl assumes V is already unpacked (has Pubkeys). + // For packed variants, use PackedCTokenDataWithVariant::unpack instead. Ok(TokenDataWithVariant { variant: self.variant.clone(), token_data: self.token_data.unpack(remaining_accounts)?, @@ -290,13 +302,14 @@ pub mod compat { impl Pack for TokenDataWithVariant where - V: AnchorSerialize + Clone + std::fmt::Debug, + V: Pack, + V::Packed: AnchorSerialize + Clone + std::fmt::Debug, { - type Packed = PackedCTokenDataWithVariant; + type Packed = PackedCTokenDataWithVariant; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { PackedCTokenDataWithVariant { - variant: self.variant.clone(), + variant: self.variant.pack(remaining_accounts), token_data: self.token_data.pack(remaining_accounts), } } @@ -304,16 +317,16 @@ pub mod compat { impl Unpack for PackedCTokenDataWithVariant where - V: Clone, + V: Unpack, { - type Unpacked = TokenDataWithVariant; + type Unpacked = TokenDataWithVariant; fn unpack( &self, remaining_accounts: &[AccountInfo], ) -> std::result::Result { Ok(TokenDataWithVariant { - variant: self.variant.clone(), + variant: self.variant.unpack(remaining_accounts)?, token_data: self.token_data.unpack(remaining_accounts)?, }) } diff --git a/sdk-libs/token-sdk/src/token/create.rs b/sdk-libs/token-sdk/src/token/create.rs index cd0c070120..b6425e82cf 100644 --- a/sdk-libs/token-sdk/src/token/create.rs +++ b/sdk-libs/token-sdk/src/token/create.rs @@ -1,7 +1,13 @@ use borsh::BorshSerialize; +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create.rs use light_token_interface::instructions::{ create_token_account::CreateTokenAccountInstructionData, extensions::CompressibleExtensionInstructionData, +======= +use light_ctoken_interface::instructions::{ + create_ctoken_account::CreateTokenAccountInstructionData, + extensions::{CompressToPubkey, CompressibleExtensionInstructionData}, +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create.rs }; use solana_account_info::AccountInfo; use solana_cpi::{invoke, invoke_signed}; @@ -84,6 +90,7 @@ impl CreateTokenAccount { } } +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create.rs /// # Create a ctoken account via CPI: /// ```rust,no_run /// # use light_token_sdk::token::{CreateTokenAccountCpi, CompressibleParamsCpi}; @@ -100,18 +107,34 @@ impl CreateTokenAccount { /// mint, /// owner, /// compressible, +======= +/// CPI builder for creating CToken accounts (vaults). +/// +/// # Example - Rent-free vault with PDA signing +/// ```rust,ignore +/// CreateCTokenAccountCpi { +/// payer: ctx.accounts.payer.to_account_info(), +/// account: ctx.accounts.vault.to_account_info(), +/// mint: ctx.accounts.mint.to_account_info(), +/// owner: ctx.accounts.vault_authority.key(), +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create.rs /// } -/// .invoke()?; -/// # Ok::<(), solana_program_error::ProgramError>(()) +/// .rent_free( +/// ctx.accounts.ctoken_config.to_account_info(), +/// ctx.accounts.rent_sponsor.to_account_info(), +/// ctx.accounts.system_program.to_account_info(), +/// &crate::ID, +/// ) +/// .invoke_signed(&[b"vault", mint.key().as_ref(), &[bump]])?; /// ``` pub struct CreateTokenAccountCpi<'info> { pub payer: AccountInfo<'info>, pub account: AccountInfo<'info>, pub mint: AccountInfo<'info>, pub owner: Pubkey, - pub compressible: CompressibleParamsCpi<'info>, } +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create.rs impl<'info> CreateTokenAccountCpi<'info> { pub fn new( payer: AccountInfo<'info>, @@ -131,9 +154,166 @@ impl<'info> CreateTokenAccountCpi<'info> { pub fn instruction(&self) -> Result { CreateTokenAccount::from(self).instruction() +======= +impl<'info> CreateCTokenAccountCpi<'info> { + /// Enable rent-free mode with compressible config. + /// + /// Returns a builder that can call `.invoke()` or `.invoke_signed(seeds)`. + /// When using `invoke_signed`, the seeds are used for both PDA signing + /// and deriving the compress_to address. + pub fn rent_free( + self, + config: AccountInfo<'info>, + sponsor: AccountInfo<'info>, + system_program: AccountInfo<'info>, + program_id: &Pubkey, + ) -> CreateCTokenAccountRentFreeCpi<'info> { + CreateCTokenAccountRentFreeCpi { + base: self, + config, + sponsor, + system_program, + program_id: *program_id, + } + } + + /// Invoke without rent-free (requires manually constructed compressible params). + pub fn invoke_with( + self, + compressible: CompressibleParamsCpi<'info>, + ) -> Result<(), ProgramError> { + LegacyCreateCTokenAccountCpi { + payer: self.payer, + account: self.account, + mint: self.mint, + owner: self.owner, + compressible, + } + .invoke() +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create.rs } + /// Invoke with signing, without rent-free (requires manually constructed compressible params). + pub fn invoke_signed_with( + self, + compressible: CompressibleParamsCpi<'info>, + signer_seeds: &[&[&[u8]]], + ) -> Result<(), ProgramError> { + LegacyCreateCTokenAccountCpi { + payer: self.payer, + account: self.account, + mint: self.mint, + owner: self.owner, + compressible, + } + .invoke_signed(signer_seeds) + } +} + +/// Rent-free enabled CToken account creation CPI. +pub struct CreateCTokenAccountRentFreeCpi<'info> { + base: CreateCTokenAccountCpi<'info>, + config: AccountInfo<'info>, + sponsor: AccountInfo<'info>, + system_program: AccountInfo<'info>, + program_id: Pubkey, +} + +impl<'info> CreateCTokenAccountRentFreeCpi<'info> { + /// Invoke CPI for non-program-owned accounts. pub fn invoke(self) -> Result<(), ProgramError> { + let defaults = CompressibleParams::default(); + + let cpi = LegacyCreateCTokenAccountCpi { + payer: self.base.payer, + account: self.base.account, + mint: self.base.mint, + owner: self.base.owner, + compressible: CompressibleParamsCpi { + compressible_config: self.config, + rent_sponsor: self.sponsor, + system_program: self.system_program, + pre_pay_num_epochs: defaults.pre_pay_num_epochs, + lamports_per_write: defaults.lamports_per_write, + compress_to_account_pubkey: None, + token_account_version: defaults.token_account_version, + compression_only: defaults.compression_only, + }, + }; + cpi.invoke() + } + + /// Invoke CPI with PDA signing for program-owned accounts. + /// + /// Seeds are used for both signing AND deriving the compress_to address. + pub fn invoke_signed(self, seeds: &[&[u8]]) -> Result<(), ProgramError> { + let defaults = CompressibleParams::default(); + + // Build CompressToPubkey from signer seeds + let bump = seeds.last().and_then(|s| s.first()).copied().unwrap_or(0); + + let seed_vecs: Vec> = seeds + .iter() + .take(seeds.len().saturating_sub(1)) + .map(|s| s.to_vec()) + .collect(); + + let compress_to = CompressToPubkey { + bump, + program_id: self.program_id.to_bytes(), + seeds: seed_vecs, + }; + + let cpi = LegacyCreateCTokenAccountCpi { + payer: self.base.payer, + account: self.base.account, + mint: self.base.mint, + owner: self.base.owner, + compressible: CompressibleParamsCpi { + compressible_config: self.config, + rent_sponsor: self.sponsor, + system_program: self.system_program, + pre_pay_num_epochs: defaults.pre_pay_num_epochs, + lamports_per_write: defaults.lamports_per_write, + compress_to_account_pubkey: Some(compress_to), + token_account_version: defaults.token_account_version, + compression_only: defaults.compression_only, + }, + }; + cpi.invoke_signed(&[seeds]) + } +} + +/// Internal legacy CPI struct with full compressible params. +struct LegacyCreateCTokenAccountCpi<'info> { + payer: AccountInfo<'info>, + account: AccountInfo<'info>, + mint: AccountInfo<'info>, + owner: Pubkey, + compressible: CompressibleParamsCpi<'info>, +} + +impl<'info> LegacyCreateCTokenAccountCpi<'info> { + fn instruction(&self) -> Result { + CreateCTokenAccount { + payer: *self.payer.key, + account: *self.account.key, + mint: *self.mint.key, + owner: self.owner, + compressible: CompressibleParams { + compressible_config: *self.compressible.compressible_config.key, + rent_sponsor: *self.compressible.rent_sponsor.key, + pre_pay_num_epochs: self.compressible.pre_pay_num_epochs, + lamports_per_write: self.compressible.lamports_per_write, + compress_to_account_pubkey: self.compressible.compress_to_account_pubkey.clone(), + token_account_version: self.compressible.token_account_version, + compression_only: self.compressible.compression_only, + }, + } + .instruction() + } + + fn invoke(self) -> Result<(), ProgramError> { let instruction = self.instruction()?; let account_infos = [ self.account, @@ -146,7 +326,7 @@ impl<'info> CreateTokenAccountCpi<'info> { invoke(&instruction, &account_infos) } - pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { + fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = self.instruction()?; let account_infos = [ self.account, @@ -159,6 +339,7 @@ impl<'info> CreateTokenAccountCpi<'info> { invoke_signed(&instruction, &account_infos, signer_seeds) } } +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create.rs impl<'info> From<&CreateTokenAccountCpi<'info>> for CreateTokenAccount { fn from(account_infos: &CreateTokenAccountCpi<'info>) -> Self { @@ -182,3 +363,5 @@ impl<'info> From<&CreateTokenAccountCpi<'info>> for CreateTokenAccount { } } } +======= +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create.rs diff --git a/sdk-libs/token-sdk/src/token/create_ata.rs b/sdk-libs/token-sdk/src/token/create_ata.rs index bf4515937a..08b3f467e9 100644 --- a/sdk-libs/token-sdk/src/token/create_ata.rs +++ b/sdk-libs/token-sdk/src/token/create_ata.rs @@ -132,6 +132,7 @@ impl CreateAssociatedTokenAccount { } } +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create_ata.rs /// # Create an associated ctoken account via CPI: /// ```rust,no_run /// # use light_token_sdk::token::{CreateAssociatedAccountCpi, CompressibleParamsCpi}; @@ -152,27 +153,225 @@ impl CreateAssociatedTokenAccount { /// bump, /// compressible, /// idempotent: true, +======= +/// CPI builder for creating CToken ATAs. +/// +/// # Example - Rent-free ATA (idempotent) +/// ```rust,ignore +/// CreateCTokenAtaCpi { +/// payer: ctx.accounts.payer.to_account_info(), +/// owner: ctx.accounts.owner.to_account_info(), +/// mint: ctx.accounts.mint.to_account_info(), +/// ata: ctx.accounts.user_ata.to_account_info(), +/// bump: params.user_ata_bump, +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs /// } +/// .idempotent() +/// .rent_free( +/// ctx.accounts.ctoken_config.to_account_info(), +/// ctx.accounts.rent_sponsor.to_account_info(), +/// ctx.accounts.system_program.to_account_info(), +/// ) /// .invoke()?; -/// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create_ata.rs pub struct CreateAssociatedAccountCpi<'info> { +======= +pub struct CreateCTokenAtaCpi<'info> { + pub payer: AccountInfo<'info>, +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs pub owner: AccountInfo<'info>, pub mint: AccountInfo<'info>, - pub payer: AccountInfo<'info>, - pub associated_token_account: AccountInfo<'info>, - pub system_program: AccountInfo<'info>, + pub ata: AccountInfo<'info>, pub bump: u8, - pub compressible: CompressibleParamsCpi<'info>, - pub idempotent: bool, } +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create_ata.rs impl<'info> CreateAssociatedAccountCpi<'info> { pub fn instruction(&self) -> Result { CreateAssociatedTokenAccount::from(self).instruction() +======= +impl<'info> CreateCTokenAtaCpi<'info> { + /// Make this an idempotent create (won't fail if ATA already exists). + pub fn idempotent(self) -> CreateCTokenAtaCpiIdempotent<'info> { + CreateCTokenAtaCpiIdempotent { base: self } +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs + } + + /// Enable rent-free mode with compressible config. + pub fn rent_free( + self, + config: AccountInfo<'info>, + sponsor: AccountInfo<'info>, + system_program: AccountInfo<'info>, + ) -> CreateCTokenAtaRentFreeCpi<'info> { + CreateCTokenAtaRentFreeCpi { + payer: self.payer, + owner: self.owner, + mint: self.mint, + ata: self.ata, + bump: self.bump, + idempotent: false, + config, + sponsor, + system_program, + } + } + + /// Invoke without rent-free (requires manually constructed compressible params). + pub fn invoke_with( + self, + compressible: CompressibleParamsCpi<'info>, + system_program: AccountInfo<'info>, + ) -> Result<(), ProgramError> { + InternalCreateAtaCpi { + owner: self.owner, + mint: self.mint, + payer: self.payer, + associated_token_account: self.ata, + system_program, + bump: self.bump, + compressible, + idempotent: false, + } + .invoke() + } +} + +/// Idempotent ATA creation (intermediate type). +pub struct CreateCTokenAtaCpiIdempotent<'info> { + base: CreateCTokenAtaCpi<'info>, +} + +impl<'info> CreateCTokenAtaCpiIdempotent<'info> { + /// Enable rent-free mode with compressible config. + pub fn rent_free( + self, + config: AccountInfo<'info>, + sponsor: AccountInfo<'info>, + system_program: AccountInfo<'info>, + ) -> CreateCTokenAtaRentFreeCpi<'info> { + CreateCTokenAtaRentFreeCpi { + payer: self.base.payer, + owner: self.base.owner, + mint: self.base.mint, + ata: self.base.ata, + bump: self.base.bump, + idempotent: true, + config, + sponsor, + system_program, + } + } + + /// Invoke without rent-free (requires manually constructed compressible params). + pub fn invoke_with( + self, + compressible: CompressibleParamsCpi<'info>, + system_program: AccountInfo<'info>, + ) -> Result<(), ProgramError> { + InternalCreateAtaCpi { + owner: self.base.owner, + mint: self.base.mint, + payer: self.base.payer, + associated_token_account: self.base.ata, + system_program, + bump: self.base.bump, + compressible, + idempotent: true, + } + .invoke() } +} + +/// Rent-free enabled CToken ATA creation CPI. +pub struct CreateCTokenAtaRentFreeCpi<'info> { + payer: AccountInfo<'info>, + owner: AccountInfo<'info>, + mint: AccountInfo<'info>, + ata: AccountInfo<'info>, + bump: u8, + idempotent: bool, + config: AccountInfo<'info>, + sponsor: AccountInfo<'info>, + system_program: AccountInfo<'info>, +} +impl<'info> CreateCTokenAtaRentFreeCpi<'info> { + /// Invoke CPI. pub fn invoke(self) -> Result<(), ProgramError> { + InternalCreateAtaCpi { + owner: self.owner, + mint: self.mint, + payer: self.payer, + associated_token_account: self.ata, + system_program: self.system_program.clone(), + bump: self.bump, + compressible: CompressibleParamsCpi::new_ata( + self.config, + self.sponsor, + self.system_program, + ), + idempotent: self.idempotent, + } + .invoke() + } + + /// Invoke CPI with signer seeds (when caller needs to sign for another account). + pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { + InternalCreateAtaCpi { + owner: self.owner, + mint: self.mint, + payer: self.payer, + associated_token_account: self.ata, + system_program: self.system_program.clone(), + bump: self.bump, + compressible: CompressibleParamsCpi::new_ata( + self.config, + self.sponsor, + self.system_program, + ), + idempotent: self.idempotent, + } + .invoke_signed(signer_seeds) + } +} + +/// Internal CPI struct for ATAs with full params. +struct InternalCreateAtaCpi<'info> { + owner: AccountInfo<'info>, + mint: AccountInfo<'info>, + payer: AccountInfo<'info>, + associated_token_account: AccountInfo<'info>, + system_program: AccountInfo<'info>, + bump: u8, + compressible: CompressibleParamsCpi<'info>, + idempotent: bool, +} + +impl<'info> InternalCreateAtaCpi<'info> { + fn instruction(&self) -> Result { + CreateAssociatedCTokenAccount { + payer: *self.payer.key, + owner: *self.owner.key, + mint: *self.mint.key, + associated_token_account: *self.associated_token_account.key, + bump: self.bump, + compressible: CompressibleParams { + compressible_config: *self.compressible.compressible_config.key, + rent_sponsor: *self.compressible.rent_sponsor.key, + pre_pay_num_epochs: self.compressible.pre_pay_num_epochs, + lamports_per_write: self.compressible.lamports_per_write, + compress_to_account_pubkey: self.compressible.compress_to_account_pubkey.clone(), + token_account_version: self.compressible.token_account_version, + compression_only: self.compressible.compression_only, + }, + idempotent: self.idempotent, + } + .instruction() + } + + fn invoke(self) -> Result<(), ProgramError> { let instruction = self.instruction()?; let account_infos = [ self.owner, @@ -186,7 +385,7 @@ impl<'info> CreateAssociatedAccountCpi<'info> { invoke(&instruction, &account_infos) } - pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { + fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = self.instruction()?; let account_infos = [ self.owner, @@ -200,6 +399,7 @@ impl<'info> CreateAssociatedAccountCpi<'info> { invoke_signed(&instruction, &account_infos, signer_seeds) } } +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create_ata.rs impl<'info> From<&CreateAssociatedAccountCpi<'info>> for CreateAssociatedTokenAccount { fn from(account_infos: &CreateAssociatedAccountCpi<'info>) -> Self { @@ -225,3 +425,5 @@ impl<'info> From<&CreateAssociatedAccountCpi<'info>> for CreateAssociatedTokenAc } } } +======= +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs diff --git a/sdk-libs/token-sdk/src/token/decompress_mint.rs b/sdk-libs/token-sdk/src/token/decompress_mint.rs index 35eb188483..dfd603d431 100644 --- a/sdk-libs/token-sdk/src/token/decompress_mint.rs +++ b/sdk-libs/token-sdk/src/token/decompress_mint.rs @@ -1,8 +1,14 @@ use light_compressed_account::instruction_data::{ compressed_proof::ValidityProof, traits::LightInstructionData, }; +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/decompress_mint.rs use light_token_interface::instructions::mint_action::{ CompressedMintWithContext, DecompressMintAction, MintActionCompressedInstructionData, +======= +use light_ctoken_interface::instructions::mint_action::{ + CompressedMintWithContext, CpiContext, DecompressMintAction, + MintActionCompressedInstructionData, +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs }; use solana_account_info::AccountInfo; use solana_cpi::{invoke, invoke_signed}; @@ -229,3 +235,239 @@ impl<'info> TryFrom<&DecompressMintCpi<'info>> for DecompressMint { }) } } + +/// Decompress a compressed mint with CPI context support. +/// +/// For use in multi-operation ixns where mints are decompressed +/// along with PDAs and token accounts using a single proof. +#[derive(Debug, Clone)] +pub struct DecompressCMintWithCpiContext { + /// Mint seed pubkey (used to derive CMint PDA) + pub mint_seed_pubkey: Pubkey, + /// Fee payer + pub payer: Pubkey, + /// Mint authority (must sign) + pub authority: Pubkey, + /// State tree for the compressed mint + pub state_tree: Pubkey, + /// Input queue for reading compressed mint + pub input_queue: Pubkey, + /// Output queue for updated compressed mint + pub output_queue: Pubkey, + /// Compressed mint with context (from indexer) + pub compressed_mint_with_context: CompressedMintWithContext, + /// Validity proof for the compressed mint + pub proof: ValidityProof, + /// Rent payment in epochs (must be >= 2) + pub rent_payment: u8, + /// Lamports for future write operations + pub write_top_up: u32, + /// CPI context account + pub cpi_context_pubkey: Pubkey, + /// CPI context flags + pub cpi_context: CpiContext, + /// Compressible config account (ctoken's config) + pub compressible_config: Pubkey, + /// Rent sponsor account (ctoken's rent sponsor) + pub rent_sponsor: Pubkey, +} + +impl DecompressCMintWithCpiContext { + pub fn instruction(self) -> Result { + // Derive CMint PDA + let (cmint_pda, cmint_bump) = find_cmint_address(&self.mint_seed_pubkey); + + // Build DecompressMintAction + let action = DecompressMintAction { + cmint_bump, + rent_payment: self.rent_payment, + write_top_up: self.write_top_up, + }; + + // Build instruction data with CPI context + let instruction_data = MintActionCompressedInstructionData::new( + self.compressed_mint_with_context, + self.proof.0, + ) + .with_decompress_mint(action) + .with_cpi_context(self.cpi_context.clone()); + + // Build account metas with compressible CMint and CPI context + // Use provided config/rent_sponsor instead of hardcoded defaults + let mut meta_config = MintActionMetaConfig::new( + self.payer, + self.authority, + self.state_tree, + self.input_queue, + self.output_queue, + ) + .with_compressible_cmint(cmint_pda, self.compressible_config, self.rent_sponsor) + .with_mint_signer_no_sign(self.mint_seed_pubkey); + + meta_config.cpi_context = Some(self.cpi_context_pubkey); + + let account_metas = meta_config.to_account_metas(); + + let data = instruction_data + .data() + .map_err(|e| ProgramError::BorshIoError(e.to_string()))?; + + Ok(Instruction { + program_id: Pubkey::new_from_array(light_ctoken_interface::CTOKEN_PROGRAM_ID), + accounts: account_metas, + data, + }) + } +} + +/// CPI struct for decompressing a mint with CPI context. +pub struct DecompressCMintCpiWithContext<'info> { + /// Mint seed account (used to derive CMint PDA, does not sign) + pub mint_seed: AccountInfo<'info>, + /// Mint authority (must sign) + pub authority: AccountInfo<'info>, + /// Fee payer + pub payer: AccountInfo<'info>, + /// CMint PDA account (writable) + pub cmint: AccountInfo<'info>, + /// CompressibleConfig account + pub compressible_config: AccountInfo<'info>, + /// Rent sponsor PDA account + pub rent_sponsor: AccountInfo<'info>, + /// State tree for the compressed mint + pub state_tree: AccountInfo<'info>, + /// Input queue for reading compressed mint + pub input_queue: AccountInfo<'info>, + /// Output queue for updated compressed mint + pub output_queue: AccountInfo<'info>, + /// CPI context account + pub cpi_context_account: AccountInfo<'info>, + /// System accounts for Light Protocol + pub system_accounts: SystemAccountInfos<'info>, + /// CToken program's CPI authority (GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy) + /// This is separate from system_accounts.cpi_authority_pda which is the calling program's authority + pub ctoken_cpi_authority: AccountInfo<'info>, + /// Compressed mint with context (from indexer) + pub compressed_mint_with_context: CompressedMintWithContext, + /// Validity proof for the compressed mint + pub proof: ValidityProof, + /// Rent payment in epochs (must be >= 2) + pub rent_payment: u8, + /// Lamports for future write operations + pub write_top_up: u32, + /// CPI context flags + pub cpi_context: CpiContext, +} + +impl<'info> DecompressCMintCpiWithContext<'info> { + pub fn instruction(&self) -> Result { + DecompressCMintWithCpiContext { + mint_seed_pubkey: *self.mint_seed.key, + payer: *self.payer.key, + authority: *self.authority.key, + state_tree: *self.state_tree.key, + input_queue: *self.input_queue.key, + output_queue: *self.output_queue.key, + compressed_mint_with_context: self.compressed_mint_with_context.clone(), + proof: self.proof, + rent_payment: self.rent_payment, + write_top_up: self.write_top_up, + cpi_context_pubkey: *self.cpi_context_account.key, + cpi_context: self.cpi_context.clone(), + compressible_config: *self.compressible_config.key, + rent_sponsor: *self.rent_sponsor.key, + } + .instruction() + } + + pub fn invoke(self) -> Result<(), ProgramError> { + let instruction = self.instruction()?; + let account_infos = self.build_account_infos(); + invoke(&instruction, &account_infos) + } + + pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { + let instruction = self.instruction()?; + let account_infos = self.build_account_infos(); + invoke_signed(&instruction, &account_infos, signer_seeds) + } + + fn build_account_infos(&self) -> Vec> { + vec![ + self.system_accounts.light_system_program.clone(), + self.mint_seed.clone(), + self.authority.clone(), + self.compressible_config.clone(), + self.cmint.clone(), + self.rent_sponsor.clone(), + self.payer.clone(), + // Use ctoken's CPI authority for the CPI, not the calling program's authority + self.ctoken_cpi_authority.clone(), + self.system_accounts.registered_program_pda.clone(), + self.system_accounts.account_compression_authority.clone(), + self.system_accounts.account_compression_program.clone(), + self.system_accounts.system_program.clone(), + self.cpi_context_account.clone(), + self.output_queue.clone(), + self.state_tree.clone(), + self.input_queue.clone(), + ] + } +} + +/// Helper to create CPI context for first write (first_set_context = true) +pub fn create_decompress_mint_cpi_context_first( + address_tree_pubkey: [u8; 32], + tree_index: u8, + queue_index: u8, +) -> CpiContext { + CpiContext { + first_set_context: true, + set_context: false, + in_tree_index: tree_index, + in_queue_index: queue_index, + out_queue_index: queue_index, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey, + } +} + +/// Helper to create CPI context for subsequent writes (set_context = true) +pub fn create_decompress_mint_cpi_context_set( + address_tree_pubkey: [u8; 32], + tree_index: u8, + queue_index: u8, +) -> CpiContext { + CpiContext { + first_set_context: false, + set_context: true, + in_tree_index: tree_index, + in_queue_index: queue_index, + out_queue_index: queue_index, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey, + } +} + +/// Helper to create CPI context for execution (both false - consumes context) +pub fn create_decompress_mint_cpi_context_execute( + address_tree_pubkey: [u8; 32], + tree_index: u8, + queue_index: u8, +) -> CpiContext { + CpiContext { + first_set_context: false, + set_context: false, + in_tree_index: tree_index, + in_queue_index: queue_index, + out_queue_index: queue_index, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey, + } +} diff --git a/sdk-libs/token-sdk/src/token/mod.rs b/sdk-libs/token-sdk/src/token/mod.rs index 9094faa6b0..c792f57459 100644 --- a/sdk-libs/token-sdk/src/token/mod.rs +++ b/sdk-libs/token-sdk/src/token/mod.rs @@ -3,10 +3,17 @@ //! //! ## Account Creation //! +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/mod.rs //! - [`CreateAssociatedTokenAccount`] - Create associated ctoken account (ATA) instruction //! - [`CreateAssociatedTokenAccountCpi`] - Create associated ctoken account (ATA) via CPI //! - [`CreateTokenAccount`] - Create ctoken account instruction //! - [`CreateTokenAccountCpi`] - Create ctoken account via CPI +======= +//! - [`CreateAssociatedCTokenAccount`] - Create associated ctoken account (ATA) instruction +//! - [`CreateCTokenAtaCpi`] - Create associated ctoken account (ATA) via CPI +//! - [`CreateCTokenAccount`] - Create ctoken account instruction +//! - [`CreateCTokenAccountCpi`] - Create ctoken account via CPI +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/mod.rs //! //! ## Transfers //! @@ -49,28 +56,53 @@ //! # Ok::<(), solana_program_error::ProgramError>(()) //! ``` //! -//! # Example: Create cToken Account CPI +//! # Example: Create rent-free ATA via CPI //! //! ```rust,ignore +<<<<<<< HEAD:sdk-libs/token-sdk/src/token/mod.rs //! use light_token_sdk::token::{CreateAssociatedTokenAccountCpi, CompressibleParamsCpi}; //! //! CreateAssociatedTokenAccountCpi { +======= +//! use light_ctoken_sdk::ctoken::CreateCTokenAtaCpi; +//! +//! CreateCTokenAtaCpi { +//! payer: ctx.accounts.payer.to_account_info(), +>>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/mod.rs //! owner: ctx.accounts.owner.to_account_info(), //! mint: ctx.accounts.mint.to_account_info(), -//! payer: ctx.accounts.payer.to_account_info(), -//! associated_token_account: ctx.accounts.ctoken_account.to_account_info(), -//! system_program: ctx.accounts.system_program.to_account_info(), +//! ata: ctx.accounts.user_ata.to_account_info(), //! bump, -//! compressible: Some(CompressibleParamsCpi::default_with_accounts( -//! ctx.accounts.compressible_config.to_account_info(), -//! ctx.accounts.rent_sponsor.to_account_info(), -//! ctx.accounts.system_program.to_account_info(), -//! )), -//! idempotent: true, //! } +//! .idempotent() +//! .rent_free( +//! ctx.accounts.ctoken_config.to_account_info(), +//! ctx.accounts.rent_sponsor.to_account_info(), +//! ctx.accounts.system_program.to_account_info(), +//! ) //! .invoke()?; //! ``` //! +//! # Example: Create rent-free vault via CPI (with PDA signing) +//! +//! ```rust,ignore +//! use light_ctoken_sdk::ctoken::CreateCTokenAccountCpi; +//! +//! CreateCTokenAccountCpi { +//! payer: ctx.accounts.payer.to_account_info(), +//! account: ctx.accounts.vault.to_account_info(), +//! mint: ctx.accounts.mint.to_account_info(), +//! owner: ctx.accounts.vault_authority.key(), +//! } +//! .rent_free( +//! ctx.accounts.ctoken_config.to_account_info(), +//! ctx.accounts.rent_sponsor.to_account_info(), +//! ctx.accounts.system_program.to_account_info(), +//! &crate::ID, +//! ) +//! .invoke_signed(&[b"vault", mint.key().as_ref(), &[bump]])?; +//! ``` +//! mod approve; mod approve_checked; @@ -139,6 +171,7 @@ pub use transfer_to_spl::{TransferToSpl, TransferToSplCpi}; /// - `account_compression_authority` - Compression authority /// - `account_compression_program` - Account Compression Program /// - `system_program` - Solana System Program +#[derive(Clone)] pub struct SystemAccountInfos<'info> { pub light_system_program: AccountInfo<'info>, pub cpi_authority_pda: AccountInfo<'info>, diff --git a/sdk-libs/token-sdk/tests/pack_test.rs b/sdk-libs/token-sdk/tests/pack_test.rs index 2f3039c725..4a42dd2eba 100644 --- a/sdk-libs/token-sdk/tests/pack_test.rs +++ b/sdk-libs/token-sdk/tests/pack_test.rs @@ -68,7 +68,7 @@ fn test_token_data_with_variant_packing() { // Pack the wrapper let packed: PackedCTokenDataWithVariant = - 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)); diff --git a/sdk-tests/csdk-anchor-derived-test/Anchor.toml b/sdk-tests/csdk-anchor-derived-test/Anchor.toml deleted file mode 100644 index 3237e0c97f..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/Anchor.toml +++ /dev/null @@ -1,18 +0,0 @@ -[features] -resolution = true -skip-lint = false - -[programs.localnet] -csdk_anchor_derived_test = "FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah" - -[registry] -url = "https://api.apr.dev" - -[provider] -cluster = "Localnet" -wallet = "~/.config/solana/id.json" - -[scripts] -test = "cargo test-sbf -p csdk-anchor-derived-test -- --nocapture" - - diff --git a/sdk-tests/csdk-anchor-derived-test/Cargo.toml b/sdk-tests/csdk-anchor-derived-test/Cargo.toml deleted file mode 100644 index 8e4fcdfd87..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "csdk-anchor-derived-test" -version = "0.1.0" -description = "Anchor program test using add_compressible_instructions-derived instructions" -edition = "2021" - -[lib] -crate-type = ["cdylib", "lib"] -name = "csdk_anchor_derived_test" - -[features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] -test-sbf = [] - -[dependencies] -light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } -light-hasher = { workspace = true, features = ["solana"] } -solana-program = { workspace = true } -solana-pubkey = { workspace = true } -light-macros = { workspace = true, features = ["solana"] } -light-sdk-macros = { workspace = true } -borsh = { workspace = true } -light-compressed-account = { workspace = true, features = ["solana"] } -anchor-lang = { workspace = true, features = ["idl-build"] } -anchor-spl = { version = "=0.31.1", git = "https://github.com/lightprotocol/anchor", rev = "d8a2b3d9", features = ["memo", "metadata", "idl-build"] } -light-token-interface = { workspace = true, features = ["anchor"] } -light-token-sdk = { workspace = true, features = ["anchor", "compressible"] } -light-token-types = { workspace = true, features = ["anchor"] } -light-compressible = { workspace = true, features = ["anchor"] } - -[dev-dependencies] -light-token-client = { workspace = true } -light-program-test = { workspace = true, features = ["devenv", "v2"] } -light-client = { workspace = true, features = ["v2"] } -light-compressible-client = { workspace = true, features = ["anchor"] } -light-test-utils = { workspace = true } -tokio = { workspace = true } -solana-sdk = { workspace = true } -solana-logger = { workspace = true } -solana-instruction = { workspace = true } -solana-pubkey = { workspace = true } -solana-signature = { workspace = true } -solana-signer = { workspace = true } -solana-keypair = { workspace = true } -solana-account = { workspace = true } -bincode = "1.3" - -[lints.rust.unexpected_cfgs] -level = "allow" -check-cfg = [ - 'cfg(target_os, values("solana"))', - 'cfg(feature, values("frozen-abi", "no-entrypoint"))', -] diff --git a/sdk-tests/csdk-anchor-derived-test/Xargo.toml b/sdk-tests/csdk-anchor-derived-test/Xargo.toml deleted file mode 100644 index 4f10b17d74..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/Xargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] - - diff --git a/sdk-tests/csdk-anchor-derived-test/package.json b/sdk-tests/csdk-anchor-derived-test/package.json deleted file mode 100644 index 9f27c61933..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@lightprotocol/csdk-anchor-derived-test", - "version": "0.1.0", - "license": "Apache-2.0", - "scripts": { - "build": "cargo build-sbf", - "test": "cargo test-sbf -p csdk-anchor-derived-test -- --nocapture" - }, - "nx": {} -} - diff --git a/sdk-tests/csdk-anchor-derived-test/src/errors.rs b/sdk-tests/csdk-anchor-derived-test/src/errors.rs deleted file mode 100644 index e7bdc66a08..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/errors.rs +++ /dev/null @@ -1,12 +0,0 @@ -use anchor_lang::prelude::ProgramError; - -#[repr(u32)] -pub enum ErrorCode { - RentRecipientMismatch, -} - -impl From for ProgramError { - fn from(e: ErrorCode) -> Self { - ProgramError::Custom(e as u32) - } -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs b/sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs deleted file mode 100644 index eae19d13f8..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs +++ /dev/null @@ -1,109 +0,0 @@ -use anchor_lang::prelude::*; - -use crate::state::*; - -#[derive(Accounts)] -#[instruction(account_data: AccountCreationData)] -pub struct CreateUserRecordAndGameSession<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - init, - payer = user, - space = 8 + UserRecord::INIT_SPACE, - seeds = [b"user_record", user.key().as_ref()], - bump, - )] - pub user_record: Account<'info, UserRecord>, - #[account( - init, - payer = user, - space = 8 + GameSession::INIT_SPACE, - seeds = [b"game_session", account_data.session_id.to_le_bytes().as_ref()], - bump, - )] - pub game_session: Account<'info, GameSession>, - - /// The mint signer used for PDA derivation - pub mint_signer: Signer<'info>, - - /// The mint authority used for PDA derivation - pub mint_authority: Signer<'info>, - - /// Compressed token program - /// CHECK: Program ID validated using LIGHT_TOKEN_PROGRAM_ID constant - pub ctoken_program: UncheckedAccount<'info>, - - /// CHECK: CPI authority of the compressed token program - pub compress_token_program_cpi_authority: UncheckedAccount<'info>, - - /// Needs to be here for the init anchor macro to work. - pub system_program: Program<'info, System>, - - /// Global compressible config - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct InitializeCompressionConfig<'info> { - #[account(mut)] - pub payer: Signer<'info>, - /// CHECK: Config PDA is created and validated by the SDK - #[account(mut)] - pub config: AccountInfo<'info>, - /// CHECK: Program data account is validated by the SDK - pub program_data: AccountInfo<'info>, - pub authority: Signer<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct UpdateCompressionConfig<'info> { - /// CHECK: Checked by SDK - #[account(mut)] - pub config: AccountInfo<'info>, - pub authority: Signer<'info>, -} - -#[derive(Accounts)] -pub struct DecompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// CHECK: checked by SDK - pub config: AccountInfo<'info>, - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - /// CHECK: anyone can pay (optional - only needed if decompressing tokens) - #[account(mut)] - pub ctoken_rent_sponsor: Option>, - /// CHECK: checked by SDK (optional - only needed if decompressing tokens) - pub ctoken_config: Option>, - /// CHECK: - #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] - pub ctoken_program: Option>, - /// CHECK: - #[account(address = solana_pubkey::pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] - pub ctoken_cpi_authority: Option>, - /// CHECK: checked by SDK - pub some_mint: AccountInfo<'info>, -} - -#[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>, -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/lib.rs b/sdk-tests/csdk-anchor-derived-test/src/lib.rs deleted file mode 100644 index b267dc487e..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/lib.rs +++ /dev/null @@ -1,291 +0,0 @@ -#![allow(deprecated)] - -use anchor_lang::prelude::*; -use light_sdk::derive_light_cpi_signer; -use light_sdk_types::CpiSigner; - -pub mod errors; -pub mod instruction_accounts; -pub mod processor; -pub mod seeds; -pub mod state; -pub mod variant; - -pub use instruction_accounts::*; -pub use state::{ - AccountCreationData, CompressionParams, GameSession, PlaceholderRecord, UserRecord, -}; -pub use variant::{CTokenAccountVariant, CompressedAccountData, CompressedAccountVariant}; - -declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); - -pub const LIGHT_CPI_SIGNER: CpiSigner = - derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); -#[program] -pub mod csdk_anchor_derived_test { - use anchor_lang::solana_program::{program::invoke, sysvar::clock::Clock}; - use light_compressed_account::instruction_data::traits::LightInstructionData; - use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - }; - use light_sdk_types::{ - cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, - }; - use light_token_interface::instructions::mint_action::{ - MintActionCompressedInstructionData, MintToCompressedAction, Recipient, - }; - use light_token_sdk::compressed_token::{ - create_compressed_mint::find_mint_address, mint_action::MintActionMetaConfig, - }; - - use super::*; - use crate::{ - errors::ErrorCode, - seeds::get_ctoken_signer_seeds, - state::{GameSession, UserRecord}, - LIGHT_CPI_SIGNER, - }; - - pub fn create_user_record_and_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, - account_data: AccountCreationData, - compression_params: CompressionParams, - ) -> Result<()> { - let user_record = &mut ctx.accounts.user_record; - let game_session = &mut ctx.accounts.game_session; - - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - user_record.owner = ctx.accounts.user.key(); - user_record.name = account_data.user_name.clone(); - user_record.score = 11; - - game_session.session_id = account_data.session_id; - game_session.player = ctx.accounts.user.key(); - game_session.game_type = account_data.game_type.clone(); - game_session.start_time = Clock::get()?.unix_timestamp as u64; - game_session.end_time = None; - game_session.score = 0; - - let cpi_accounts = CpiAccounts::new_with_config( - ctx.accounts.user.as_ref(), - ctx.remaining_accounts, - CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), - ); - let cpi_context_pubkey = cpi_accounts.cpi_context().unwrap().key(); - let cpi_context_account = cpi_accounts.cpi_context().unwrap(); - - let user_new_address_params = compression_params - .user_address_tree_info - .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); - let game_new_address_params = compression_params - .game_address_tree_info - .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(1)); - - let mut all_compressed_infos = Vec::new(); - - let user_record_info = user_record.to_account_info(); - let user_record_data_mut = &mut **user_record; - let user_compressed_info = prepare_compressed_account_on_init::( - &user_record_info, - user_record_data_mut, - &config, - compression_params.user_compressed_address, - user_new_address_params, - compression_params.user_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, - )?; - all_compressed_infos.push(user_compressed_info); - - let game_session_info = game_session.to_account_info(); - let game_session_data_mut = &mut **game_session; - let game_compressed_info = prepare_compressed_account_on_init::( - &game_session_info, - game_session_data_mut, - &config, - compression_params.game_compressed_address, - game_new_address_params, - compression_params.game_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, - )?; - all_compressed_infos.push(game_compressed_info); - - let cpi_context_accounts = CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts.authority().unwrap(), - cpi_context: cpi_context_account, - cpi_signer: LIGHT_CPI_SIGNER, - }; - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, compression_params.proof) - .with_new_addresses(&[user_new_address_params, game_new_address_params]) - .with_account_infos(&all_compressed_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(cpi_context_accounts)?; - - let mint = find_mint_address(&ctx.accounts.mint_signer.key()).0; - let (_, token_account_address) = get_ctoken_signer_seeds(&ctx.accounts.user.key(), &mint); - - let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; - let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; - - // Build instruction data using the correct API - let proof = compression_params.proof.0.unwrap_or_default(); - let instruction_data = MintActionCompressedInstructionData::new_mint( - 0, // root_index for new addresses - proof, - compression_params.mint_with_context.mint.clone().unwrap(), - ) - .with_mint_to_compressed(MintToCompressedAction { - token_account_version: 3, - recipients: vec![Recipient::new(token_account_address, 1000)], - }) - .with_cpi_context( - light_token_interface::instructions::mint_action::CpiContext { - address_tree_pubkey: address_tree_pubkey.to_bytes(), - set_context: false, - first_set_context: false, - in_tree_index: 1, - in_queue_index: 0, - out_queue_index: 0, - token_out_queue_index: 0, - assigned_account_index: 2, - read_only_address_trees: [0; 4], - }, - ); - - // Build account metas - let mut config = MintActionMetaConfig::new_create_mint( - ctx.accounts.user.key(), // fee_payer - ctx.accounts.mint_authority.key(), // authority (mint authority) - ctx.accounts.mint_signer.key(), // mint_signer - address_tree_pubkey, - output_queue, - ) - .with_mint_compressed_tokens(); - - config.cpi_context = Some(cpi_context_pubkey); - - let account_metas = config.to_account_metas(); - - // Serialize instruction data - let data = instruction_data.data().map_err(ProgramError::from)?; - - // Build mint action instruction - let mint_action_instruction = solana_program::instruction::Instruction { - program_id: light_token_interface::LIGHT_TOKEN_PROGRAM_ID.into(), - accounts: account_metas, - data, - }; - - let mut account_infos = cpi_accounts.to_account_infos(); - account_infos.push( - ctx.accounts - .compress_token_program_cpi_authority - .to_account_info(), - ); - account_infos.push(ctx.accounts.ctoken_program.to_account_info()); - account_infos.push(ctx.accounts.mint_authority.to_account_info()); - account_infos.push(ctx.accounts.mint_signer.to_account_info()); - account_infos.push(ctx.accounts.user.to_account_info()); - - invoke(&mint_action_instruction, &account_infos)?; - - user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; - game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; - - Ok(()) - } - - pub fn initialize_compression_config<'info>( - ctx: Context<'_, '_, '_, 'info, InitializeCompressionConfig<'info>>, - rent_sponsor: Pubkey, - address_space: Vec, - ) -> Result<()> { - let compression_authority = ctx.accounts.authority.key(); - let rent_config = light_compressible::rent::RentConfig::default(); - let write_top_up: u32 = 5_000; - light_sdk::compressible::process_initialize_compression_config_checked( - &ctx.accounts.config.to_account_info(), - &ctx.accounts.authority.to_account_info(), - &ctx.accounts.program_data.to_account_info(), - &rent_sponsor, - &compression_authority, - rent_config, - write_top_up, - address_space, - 0, - &ctx.accounts.payer.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - &crate::ID, - )?; - Ok(()) - } - - pub fn update_compression_config<'info>( - ctx: Context<'_, '_, '_, 'info, UpdateCompressionConfig<'info>>, - new_rent_sponsor: Option, - new_compression_authority: Option, - new_rent_config: Option, - new_write_top_up: Option, - new_address_space: Option>, - new_update_authority: Option, - ) -> Result<()> { - light_sdk::compressible::process_update_compression_config( - ctx.accounts.config.as_ref(), - ctx.accounts.authority.as_ref(), - new_update_authority.as_ref(), - new_rent_sponsor.as_ref(), - new_compression_authority.as_ref(), - new_rent_config, - new_write_top_up, - new_address_space, - &crate::ID, - )?; - Ok(()) - } - - pub fn decompress_accounts_idempotent<'info>( - ctx: Context<'_, '_, 'info, 'info, DecompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - crate::processor::process_decompress_accounts_idempotent( - ctx.accounts, - ctx.remaining_accounts, - compressed_accounts, - proof, - system_accounts_offset, - ) - } - - pub fn compress_accounts_idempotent<'info>( - ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, - _proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec< - light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - >, - system_accounts_offset: u8, - ) -> Result<()> { - crate::processor::process_compress_accounts_idempotent( - ctx.accounts, - ctx.remaining_accounts, - compressed_accounts, - system_accounts_offset, - ) - } -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/processor.rs b/sdk-tests/csdk-anchor-derived-test/src/processor.rs deleted file mode 100644 index 82c3e3847e..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/processor.rs +++ /dev/null @@ -1,326 +0,0 @@ -use anchor_lang::prelude::*; -use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo; -use light_sdk::{ - compressible::{compress_account::prepare_account_for_compression, CompressibleConfig}, - cpi::v2::CpiAccounts, - instruction::{account_meta::CompressedAccountMetaNoLamportsNoAddress, ValidityProof}, - LightDiscriminator, -}; -use light_token_sdk::compat::PackedCTokenData; - -use crate::{ - instruction_accounts::{CompressAccountsIdempotent, DecompressAccountsIdempotent}, - state::{GameSession, PlaceholderRecord, UserRecord}, - variant::{CTokenAccountVariant, CompressedAccountData, CompressedAccountVariant}, - LIGHT_CPI_SIGNER, -}; - -impl light_sdk::compressible::HasTokenVariant for CompressedAccountData { - fn is_packed_token(&self) -> bool { - matches!(self.data, CompressedAccountVariant::PackedCTokenData(_)) - } -} - -/// Empty struct since this test doesn't use data.* fields in PDA seeds -#[derive(Default)] -pub struct SeedParams; - -impl<'info> light_sdk::compressible::DecompressContext<'info> - for DecompressAccountsIdempotent<'info> -{ - type CompressedData = CompressedAccountData; - type PackedTokenData = PackedCTokenData; - type CompressedMeta = CompressedAccountMetaNoLamportsNoAddress; - type SeedParams = SeedParams; - - fn fee_payer(&self) -> &AccountInfo<'info> { - self.fee_payer.as_ref() - } - - fn config(&self) -> &AccountInfo<'info> { - &self.config - } - - fn rent_sponsor(&self) -> &AccountInfo<'info> { - self.rent_sponsor.as_ref() - } - - fn token_rent_sponsor(&self) -> Option<&AccountInfo<'info>> { - self.ctoken_rent_sponsor.as_ref() - } - - fn token_program(&self) -> Option<&AccountInfo<'info>> { - self.ctoken_program.as_ref() - } - - fn token_cpi_authority(&self) -> Option<&AccountInfo<'info>> { - self.ctoken_cpi_authority.as_ref() - } - - fn token_config(&self) -> Option<&AccountInfo<'info>> { - self.ctoken_config.as_ref() - } - - fn collect_pda_and_token<'b>( - &self, - cpi_accounts: &CpiAccounts<'b, 'info>, - address_space: Pubkey, - compressed_accounts: Vec, - solana_accounts: &[AccountInfo<'info>], - _seed_params: Option<&Self::SeedParams>, - ) -> std::result::Result< - ( - Vec, - Vec<(Self::PackedTokenData, Self::CompressedMeta)>, - ), - ProgramError, - > { - let post_system_offset = cpi_accounts.system_accounts_end_offset(); - let all_infos = cpi_accounts.account_infos(); - let post_system_accounts = &all_infos[post_system_offset..]; - - let mut compressed_pda_infos = Vec::new(); - let mut compressed_token_accounts = Vec::new(); - - for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { - let meta = compressed_data.meta; - match compressed_data.data { - CompressedAccountVariant::PackedUserRecord(packed) => { - light_sdk::compressible::handle_packed_pda_variant::< - UserRecord, - _, - DecompressAccountsIdempotent<'info>, - SeedParams, - >( - self.rent_sponsor.as_ref(), - cpi_accounts, - address_space, - &solana_accounts[i], - i, - &packed, - &meta, - post_system_accounts, - &mut compressed_pda_infos, - &crate::ID, - self, - None, - )?; - } - CompressedAccountVariant::PackedGameSession(packed) => { - light_sdk::compressible::handle_packed_pda_variant::< - GameSession, - _, - DecompressAccountsIdempotent<'info>, - SeedParams, - >( - self.rent_sponsor.as_ref(), - cpi_accounts, - address_space, - &solana_accounts[i], - i, - &packed, - &meta, - post_system_accounts, - &mut compressed_pda_infos, - &crate::ID, - self, - None, - )?; - } - CompressedAccountVariant::PackedPlaceholderRecord(packed) => { - light_sdk::compressible::handle_packed_pda_variant::< - PlaceholderRecord, - _, - DecompressAccountsIdempotent<'info>, - SeedParams, - >( - self.rent_sponsor.as_ref(), - cpi_accounts, - address_space, - &solana_accounts[i], - i, - &packed, - &meta, - post_system_accounts, - &mut compressed_pda_infos, - &crate::ID, - self, - None, - )?; - } - CompressedAccountVariant::PackedCTokenData(mut data) => { - data.token_data.version = 3; - compressed_token_accounts.push((data, meta)); - } - CompressedAccountVariant::UserRecord(_) - | CompressedAccountVariant::GameSession(_) - | CompressedAccountVariant::PlaceholderRecord(_) - | CompressedAccountVariant::CTokenData(_) => { - unreachable!("Unpacked variants should not appear during decompression") - } - } - } - - Ok((compressed_pda_infos, compressed_token_accounts)) - } - - fn process_tokens<'b>( - &self, - _remaining_accounts: &[AccountInfo<'info>], - _fee_payer: &AccountInfo<'info>, - _token_program: &AccountInfo<'info>, - _token_rent_sponsor: &AccountInfo<'info>, - _token_cpi_authority: &AccountInfo<'info>, - _token_config: &AccountInfo<'info>, - _config: &AccountInfo<'info>, - token_accounts: Vec<(Self::PackedTokenData, Self::CompressedMeta)>, - proof: light_sdk::instruction::ValidityProof, - cpi_accounts: &CpiAccounts<'b, 'info>, - post_system_accounts: &[AccountInfo<'info>], - has_pdas: bool, - ) -> std::result::Result<(), ProgramError> { - if token_accounts.is_empty() { - return Ok(()); - } - - light_token_sdk::compressible::process_decompress_tokens_runtime::( - self, - _remaining_accounts, - _fee_payer, - _token_program, - _token_rent_sponsor, - _token_cpi_authority, - _token_config, - _config, - token_accounts, - proof, - cpi_accounts, - post_system_accounts, - has_pdas, - &crate::ID, - )?; - - Ok(()) - } -} - -#[inline(never)] -pub fn process_decompress_accounts_idempotent<'info>( - accounts: &DecompressAccountsIdempotent<'info>, - remaining_accounts: &[AccountInfo<'info>], - compressed_accounts: Vec, - proof: ValidityProof, - 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, // No seed params needed for manual implementation - ) - .map_err(|e| e.into()) -} - -impl<'info> light_sdk::compressible::CompressContext<'info> for CompressAccountsIdempotent<'info> { - fn fee_payer(&self) -> &AccountInfo<'info> { - self.fee_payer.as_ref() - } - - fn config(&self) -> &AccountInfo<'info> { - &self.config - } - - fn rent_sponsor(&self) -> &AccountInfo<'info> { - &self.rent_sponsor - } - - fn compression_authority(&self) -> &AccountInfo<'info> { - &self.compression_authority - } - - fn compress_pda_account( - &self, - account_info: &AccountInfo<'info>, - meta: &CompressedAccountMetaNoLamportsNoAddress, - cpi_accounts: &CpiAccounts<'_, 'info>, - compression_config: &CompressibleConfig, - program_id: &Pubkey, - ) -> std::result::Result, ProgramError> { - let data = account_info.try_borrow_data()?; - let discriminator = &data[0..8]; - - match discriminator { - d if d == UserRecord::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = UserRecord::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - program_id, - account_info, - &mut account_data, - meta, - cpi_accounts, - &compression_config.address_space, - )?; - Ok(Some(compressed_info)) - } - d if d == GameSession::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = GameSession::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - program_id, - account_info, - &mut account_data, - meta, - cpi_accounts, - &compression_config.address_space, - )?; - Ok(Some(compressed_info)) - } - d if d == PlaceholderRecord::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = PlaceholderRecord::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - program_id, - account_info, - &mut account_data, - meta, - cpi_accounts, - &compression_config.address_space, - )?; - Ok(Some(compressed_info)) - } - _ => Err(ProgramError::InvalidAccountData), - } - } -} - -#[inline(never)] -pub fn process_compress_accounts_idempotent<'info>( - accounts: &CompressAccountsIdempotent<'info>, - remaining_accounts: &[AccountInfo<'info>], - compressed_accounts: Vec, - system_accounts_offset: u8, -) -> Result<()> { - light_sdk::compressible::process_compress_pda_accounts_idempotent( - accounts, - remaining_accounts, - compressed_accounts, - system_accounts_offset, - LIGHT_CPI_SIGNER, - &crate::ID, - ) - .map_err(|e| e.into()) -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/seeds.rs b/sdk-tests/csdk-anchor-derived-test/src/seeds.rs deleted file mode 100644 index 532aef3ef8..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/seeds.rs +++ /dev/null @@ -1,47 +0,0 @@ -use anchor_lang::prelude::Pubkey; - -pub fn get_user_record_seeds(owner: &Pubkey) -> (Vec>, Pubkey) { - let seeds: &[&[u8]] = &[b"user_record", owner.as_ref()]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - for seed in seeds { - seeds_vec.push(seed.to_vec()); - } - seeds_vec.push(vec![bump]); - (seeds_vec, pda) -} - -pub fn get_game_session_seeds(session_id: u64) -> (Vec>, Pubkey) { - let session_id_bytes = session_id.to_le_bytes(); - let seeds: &[&[u8]] = &[b"game_session", session_id_bytes.as_ref()]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - for seed in seeds { - seeds_vec.push(seed.to_vec()); - } - seeds_vec.push(vec![bump]); - (seeds_vec, pda) -} - -pub fn get_placeholder_record_seeds(placeholder_id: u64) -> (Vec>, Pubkey) { - let placeholder_id_bytes = placeholder_id.to_le_bytes(); - let seeds: &[&[u8]] = &[b"placeholder_record", placeholder_id_bytes.as_ref()]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - for seed in seeds { - seeds_vec.push(seed.to_vec()); - } - seeds_vec.push(vec![bump]); - (seeds_vec, pda) -} - -pub fn get_ctoken_signer_seeds(user: &Pubkey, mint: &Pubkey) -> (Vec>, Pubkey) { - let seeds: &[&[u8]] = &[b"ctoken_signer", user.as_ref(), mint.as_ref()]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - for seed in seeds { - seeds_vec.push(seed.to_vec()); - } - seeds_vec.push(vec![bump]); - (seeds_vec, pda) -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/state.rs b/sdk-tests/csdk-anchor-derived-test/src/state.rs deleted file mode 100644 index 262667ff61..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/state.rs +++ /dev/null @@ -1,126 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::{ - compressible::CompressionInfo, - instruction::{PackedAddressTreeInfo, ValidityProof}, - LightDiscriminator, LightHasher, -}; -use light_sdk_macros::{Compressible, CompressiblePack}; -use light_token_interface::instructions::mint_action::CompressedMintWithContext; - -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] -#[account] -pub struct UserRecord { - #[skip] - pub compression_info: Option, - #[hash] - pub owner: Pubkey, - #[max_len(32)] - pub name: String, - pub score: u64, -} - -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] -#[compress_as(start_time = 0, end_time = None, score = 0)] -#[account] -pub struct GameSession { - #[skip] - pub compression_info: Option, - pub session_id: u64, - #[hash] - pub player: Pubkey, - #[max_len(32)] - pub game_type: String, - pub start_time: u64, - pub end_time: Option, - pub score: u64, -} - -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] -#[account] -pub struct PlaceholderRecord { - #[skip] - pub compression_info: Option, - #[hash] - pub owner: Pubkey, - #[max_len(32)] - pub name: String, - pub placeholder_id: u64, -} - -// Implement PdaSeedDerivation for UserRecord -impl light_sdk::compressible::PdaSeedDerivation for UserRecord { - fn derive_pda_seeds_with_accounts( - &self, - _program_id: &Pubkey, - _accounts: &A, - _seed_params: &S, - ) -> std::result::Result<(Vec>, Pubkey), anchor_lang::prelude::ProgramError> { - Ok(crate::seeds::get_user_record_seeds(&self.owner)) - } -} - -// Implement PdaSeedDerivation for GameSession -impl light_sdk::compressible::PdaSeedDerivation for GameSession { - fn derive_pda_seeds_with_accounts( - &self, - _program_id: &Pubkey, - _accounts: &A, - _seed_params: &S, - ) -> std::result::Result<(Vec>, Pubkey), anchor_lang::prelude::ProgramError> { - Ok(crate::seeds::get_game_session_seeds(self.session_id)) - } -} - -// Implement PdaSeedDerivation for PlaceholderRecord -impl light_sdk::compressible::PdaSeedDerivation for PlaceholderRecord { - fn derive_pda_seeds_with_accounts( - &self, - _program_id: &Pubkey, - _accounts: &A, - _seed_params: &S, - ) -> std::result::Result<(Vec>, Pubkey), anchor_lang::prelude::ProgramError> { - Ok(crate::seeds::get_placeholder_record_seeds( - self.placeholder_id, - )) - } -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct AccountCreationData { - pub user_name: String, - pub session_id: u64, - pub game_type: String, - pub mint_name: String, - pub mint_symbol: String, - pub mint_uri: String, - pub mint_decimals: u8, - pub mint_supply: u64, - pub mint_update_authority: Option, - pub mint_freeze_authority: Option, - pub additional_metadata: Option>, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct TokenAccountInfo { - pub user: Pubkey, - pub mint: Pubkey, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct CompressionParams { - pub proof: ValidityProof, - pub user_compressed_address: [u8; 32], - pub user_address_tree_info: PackedAddressTreeInfo, - pub user_output_state_tree_index: u8, - pub game_compressed_address: [u8; 32], - pub game_address_tree_info: PackedAddressTreeInfo, - pub game_output_state_tree_index: u8, - pub mint_bump: u8, - pub mint_with_context: CompressedMintWithContext, -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/variant.rs b/sdk-tests/csdk-anchor-derived-test/src/variant.rs deleted file mode 100644 index 4dada3bd76..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/variant.rs +++ /dev/null @@ -1,173 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::{ - account::Size, - compressible::{CompressionInfo, HasCompressionInfo, Pack as SdkPack, Unpack as SdkUnpack}, - instruction::{account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts}, - LightDiscriminator, -}; -use light_token_sdk::{ - compat::{CTokenData, PackedCTokenData}, - pack::Pack as TokenPack, -}; - -use crate::{ - instruction_accounts::DecompressAccountsIdempotent, - seeds::get_ctoken_signer_seeds, - state::{ - GameSession, PackedGameSession, PackedPlaceholderRecord, PackedUserRecord, - PlaceholderRecord, UserRecord, - }, -}; - -#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] -#[repr(u8)] -pub enum CTokenAccountVariant { - CTokenSigner = 0, -} - -impl light_token_sdk::compressible::TokenSeedProvider for CTokenAccountVariant { - type Accounts<'info> = DecompressAccountsIdempotent<'info>; - - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - _remaining_accounts: &'a [AccountInfo<'info>], - ) -> std::result::Result<(Vec>, Pubkey), ProgramError> { - match self { - CTokenAccountVariant::CTokenSigner => { - // Use the same convention as the mint/init path: ("ctoken_signer", user, mint) - std::result::Result::<(Vec>, Pubkey), ProgramError>::Ok( - get_ctoken_signer_seeds(&accounts.fee_payer.key(), &accounts.some_mint.key()), - ) - } - } - } - - fn get_authority_seeds<'a, 'info>( - &self, - _accounts: &'a Self::Accounts<'info>, - _remaining_accounts: &'a [AccountInfo<'info>], - ) -> std::result::Result<(Vec>, Pubkey), ProgramError> { - // Not used by the decompression runtime in this test. - std::result::Result::<(Vec>, Pubkey), ProgramError>::Err( - ProgramError::InvalidAccountData, - ) - } -} - -#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] -pub enum CompressedAccountVariant { - UserRecord(UserRecord), - PackedUserRecord(PackedUserRecord), - GameSession(GameSession), - PackedGameSession(PackedGameSession), - PlaceholderRecord(PlaceholderRecord), - PackedPlaceholderRecord(PackedPlaceholderRecord), - PackedCTokenData(PackedCTokenData), - CTokenData(CTokenData), -} - -impl Default for CompressedAccountVariant { - fn default() -> Self { - Self::UserRecord(UserRecord::default()) - } -} - -impl LightDiscriminator for CompressedAccountVariant { - const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; - const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; -} - -impl HasCompressionInfo for CompressedAccountVariant { - fn compression_info(&self) -> &CompressionInfo { - match self { - Self::UserRecord(data) => data.compression_info(), - Self::GameSession(data) => data.compression_info(), - Self::PlaceholderRecord(data) => data.compression_info(), - _ => unreachable!(), - } - } - - fn compression_info_mut(&mut self) -> &mut CompressionInfo { - match self { - Self::UserRecord(data) => data.compression_info_mut(), - Self::GameSession(data) => data.compression_info_mut(), - Self::PlaceholderRecord(data) => data.compression_info_mut(), - _ => unreachable!(), - } - } - - fn compression_info_mut_opt(&mut self) -> &mut Option { - match self { - Self::UserRecord(data) => data.compression_info_mut_opt(), - Self::GameSession(data) => data.compression_info_mut_opt(), - Self::PlaceholderRecord(data) => data.compression_info_mut_opt(), - _ => unreachable!(), - } - } - - fn set_compression_info_none(&mut self) { - match self { - Self::UserRecord(data) => data.set_compression_info_none(), - Self::GameSession(data) => data.set_compression_info_none(), - Self::PlaceholderRecord(data) => data.set_compression_info_none(), - _ => unreachable!(), - } - } -} - -impl Size for CompressedAccountVariant { - fn size(&self) -> usize { - match self { - Self::UserRecord(data) => data.size(), - Self::GameSession(data) => data.size(), - Self::PlaceholderRecord(data) => data.size(), - _ => unreachable!(), - } - } -} - -impl SdkPack for CompressedAccountVariant { - type Packed = Self; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - match self { - Self::UserRecord(data) => Self::PackedUserRecord(data.pack(remaining_accounts)), - Self::GameSession(data) => Self::PackedGameSession(data.pack(remaining_accounts)), - Self::PlaceholderRecord(data) => { - Self::PackedPlaceholderRecord(data.pack(remaining_accounts)) - } - Self::CTokenData(data) => { - Self::PackedCTokenData(TokenPack::pack(data, remaining_accounts)) - } - _ => unreachable!(), - } - } -} - -impl SdkUnpack for CompressedAccountVariant { - type Unpacked = Self; - - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - match self { - Self::PackedUserRecord(data) => Ok(Self::UserRecord(data.unpack(remaining_accounts)?)), - Self::PackedGameSession(data) => { - Ok(Self::GameSession(data.unpack(remaining_accounts)?)) - } - Self::PackedPlaceholderRecord(data) => { - Ok(Self::PlaceholderRecord(data.unpack(remaining_accounts)?)) - } - Self::PackedCTokenData(data) => Ok(Self::PackedCTokenData(data.clone())), - _ => unreachable!(), - } - } -} - -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct CompressedAccountData { - pub meta: CompressedAccountMetaNoLamportsNoAddress, - pub data: CompressedAccountVariant, -} diff --git a/sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs b/sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs deleted file mode 100644 index 7a6c487966..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs +++ /dev/null @@ -1,706 +0,0 @@ -use anchor_lang::{AccountDeserialize, AnchorDeserialize, InstructionData, ToAccountMetas}; -use csdk_anchor_derived_test::{AccountCreationData, CompressionParams, GameSession, UserRecord}; -use light_compressed_account::address::derive_address; -use light_macros::pubkey; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - AddressWithTree, Indexer, ProgramTestConfig, Rpc, -}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; -use light_token_interface::{ - instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, - state::CompressedMintMetadata, -}; -use light_token_sdk::compressed_token::create_compressed_mint::{ - derive_mint_compressed_address, find_mint_address, -}; -use light_token_types::CPI_AUTHORITY_PDA; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx")]; -const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); - -#[tokio::test] -async fn test_create_decompress_compress() { - let program_id = csdk_anchor_derived_test::ID; - let mut config = - ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_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 config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &light_compressible_client::compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let session_id = 42424u64; - let (user_record_pda, _user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - let (game_session_pda, _game_bump) = Pubkey::find_program_address( - &[b"game_session", session_id.to_le_bytes().as_ref()], - &program_id, - ); - - let mint_signer_pubkey = create_user_record_and_game_session( - &mut rpc, - &payer, - &program_id, - &config_pda, - &user_record_pda, - &game_session_pda, - session_id, - ) - .await; - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_user_record = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_user_record.address, - Some(user_compressed_address) - ); - assert!(compressed_user_record.data.is_some()); - - let user_buf = compressed_user_record.data.unwrap().data; - let user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); - - assert_eq!(user_record.name, "Combined User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, payer.pubkey()); - assert!(user_record.compression_info.is_none()); - - let compressed_game_session = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_game_session.address, - Some(game_compressed_address) - ); - assert!(compressed_game_session.data.is_some()); - - let game_buf = compressed_game_session.data.unwrap().data; - let game_session = GameSession::deserialize(&mut &game_buf[..]).unwrap(); - assert_eq!(game_session.session_id, session_id); - assert_eq!(game_session.game_type, "Test Game"); - assert_eq!(game_session.player, payer.pubkey()); - assert_eq!(game_session.score, 0); - assert!(game_session.compression_info.is_none()); - - let spl_mint = find_mint_address(&mint_signer_pubkey).0; - let (_, token_account_address) = - csdk_anchor_derived_test::seeds::get_ctoken_signer_seeds(&payer.pubkey(), &spl_mint); - - let ctoken_accounts = rpc - .get_compressed_token_accounts_by_owner(&token_account_address, None, None) - .await - .unwrap() - .value; - - assert!( - !ctoken_accounts.items.is_empty(), - "Should have compressed token accounts" - ); - - // Test decompress PDAs (UserRecord + GameSession) - // Note: Light Token decompression works but requires manual instruction building - // because the client helper doesn't handle mixed PDA+token packing correctly - rpc.warp_to_slot(100).unwrap(); - - decompress_pdas( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &game_session_pda, - session_id, - 100, - ) - .await; - - // Test compress PDAs after decompression - rpc.warp_to_slot(200).unwrap(); - - compress_pdas( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &game_session_pda, - session_id, - ) - .await; -} - -#[tokio::test] -async fn test_auto_compress_on_warp_forward() { - use light_compressible::rent::SLOTS_PER_EPOCH; - let program_id = csdk_anchor_derived_test::ID; - let config = - ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_derived_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - // Initialize compressible config - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &light_compressible_client::compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await - .expect("Initialize config should succeed"); - - // PDAs - let session_id = 5555u64; - let (user_record_pda, _) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - let (game_session_pda, _) = Pubkey::find_program_address( - &[b"game_session", session_id.to_le_bytes().as_ref()], - &program_id, - ); - - // Create + compress initial state via helper (combined create path) - let _mint_signer_pubkey = create_user_record_and_game_session( - &mut rpc, - &payer, - &program_id, - &config_pda, - &user_record_pda, - &game_session_pda, - session_id, - ) - .await; - - // Decompress both PDAs - rpc.warp_to_slot(100).unwrap(); - decompress_pdas( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &game_session_pda, - session_id, - 100, - ) - .await; - - // Warp two epochs to ensure PDAs are compressible - rpc.warp_slot_forward(SLOTS_PER_EPOCH * 2).await.unwrap(); - - // Also invoke auto-compress directly to ensure it's executed in this test context - light_program_test::compressible::auto_compress_program_pdas(&mut rpc, program_id) - .await - .unwrap(); - - // After auto-compress, PDAs should be closed or emptied - let user_acc = rpc.get_account(user_record_pda).await.unwrap(); - let game_acc = rpc.get_account(game_session_pda).await.unwrap(); - let user_closed = user_acc.is_none() - || user_acc - .as_ref() - .map(|a| a.data.is_empty() || a.lamports == 0) - .unwrap_or(true); - let game_closed = game_acc.is_none() - || game_acc - .as_ref() - .map(|a| a.data.is_empty() || a.lamports == 0) - .unwrap_or(true); - assert!( - user_closed && game_closed, - "Auto-compress should close PDAs" - ); -} - -#[allow(clippy::too_many_arguments)] -async fn decompress_pdas( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, - expected_slot: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - // Get compressed PDA accounts - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - let c_game_pda = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let user_account_data = c_user_pda.data.as_ref().unwrap(); - let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); - - let game_account_data = c_game_pda.data.as_ref().unwrap(); - let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![c_user_pda.hash, c_game_pda.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - program_id, - &light_compressible_client::compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[*user_record_pda, *game_session_pda], - &[ - ( - c_user_pda.clone(), - csdk_anchor_derived_test::CompressedAccountVariant::UserRecord(c_user_record), - ), - ( - c_game_pda.clone(), - csdk_anchor_derived_test::CompressedAccountVariant::GameSession(c_game_session), - ), - ], - &csdk_anchor_derived_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: None, - ctoken_config: None, - ctoken_program: None, - ctoken_cpi_authority: None, - some_mint: payer.pubkey(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!(result.is_ok(), "Decompress PDAs transaction should succeed"); - - // Verify user record decompressed - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.is_some(), - "User PDA should exist after decompression" - ); - let decompressed_user_record = - UserRecord::try_deserialize(&mut &user_pda_account.unwrap().data[..]).unwrap(); - assert_eq!(decompressed_user_record.name, "Combined User"); - assert_eq!(decompressed_user_record.score, 11); - assert_eq!(decompressed_user_record.owner, payer.pubkey()); - assert!(!decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - // Verify game session decompressed - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_pda_account.is_some(), - "Game PDA should exist after decompression" - ); - let decompressed_game_session = - GameSession::try_deserialize(&mut &game_pda_account.unwrap().data[..]).unwrap(); - assert_eq!(decompressed_game_session.session_id, session_id); - assert_eq!(decompressed_game_session.game_type, "Test Game"); - assert_eq!(decompressed_game_session.player, payer.pubkey()); - assert!(!decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - // Verify compressed PDA accounts are empty - let compressed_user = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - assert!( - compressed_user.data.unwrap().data.is_empty(), - "Compressed user should be empty after decompression" - ); - - let compressed_game = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - assert!( - compressed_game.data.unwrap().data.is_empty(), - "Compressed game should be empty after decompression" - ); -} - -#[allow(clippy::too_many_arguments)] -async fn compress_pdas( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - // Get PDA accounts - let _user_pda_account = rpc - .get_account(*user_record_pda) - .await - .unwrap() - .expect("User PDA should exist before compression"); - let _game_pda_account = rpc - .get_account(*game_session_pda) - .await - .unwrap() - .expect("Game PDA should exist before compression"); - - // Get compressed account hashes for proof - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_user = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - let compressed_game = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let _rpc_result = rpc - .get_validity_proof( - vec![compressed_user.hash, compressed_game.hash], - vec![], - None, - ) - .await - .unwrap() - .value; - - // TODO: remove in separate pr - // let instruction = - // light_compressible_client::compressible_instruction::compress_accounts_idempotent( - // program_id, - // csdk_anchor_derived_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - // &[*user_record_pda, *game_session_pda], - // &[user_pda_account, game_pda_account], - // &csdk_anchor_derived_test::accounts::CompressAccountsIdempotent { - // fee_payer: payer.pubkey(), - // config: CompressibleConfig::derive_pda(program_id, 0).0, - // rent_sponsor: RENT_SPONSOR, - // compression_authority: payer.pubkey(), - // } - // .to_account_metas(None), - // rpc_result, - // ) - // .unwrap(); - - // let result = rpc - // .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - // .await; - - // assert!(result.is_ok(), "Compress PDAs transaction should succeed"); - - rpc.warp_slot_forward(light_compressible::rent::SLOTS_PER_EPOCH * 2) - .await - .unwrap(); - - // Verify PDAs are closed - let user_pda_after = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_after.is_none(), - "User PDA should be closed after compression" - ); - - let game_pda_after = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_pda_after.is_none(), - "Game PDA should be closed after compression" - ); - - // Verify compressed PDA accounts have data - let compressed_user_after = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - assert_eq!(compressed_user_after.address, Some(user_compressed_address)); - let user_buf = compressed_user_after.data.unwrap().data; - let user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); - assert_eq!(user_record.name, "Combined User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, payer.pubkey()); - assert!(user_record.compression_info.is_none()); - - let compressed_game_after = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - assert_eq!(compressed_game_after.address, Some(game_compressed_address)); - let game_buf = compressed_game_after.data.unwrap().data; - let game_session = GameSession::deserialize(&mut &game_buf[..]).unwrap(); - assert_eq!(game_session.session_id, session_id); - assert_eq!(game_session.game_type, "Test Game"); - assert!(game_session.compression_info.is_none()); -} - -#[allow(clippy::too_many_arguments)] -pub async fn create_user_record_and_game_session( - rpc: &mut LightProgramTest, - user: &Keypair, - program_id: &Pubkey, - config_pda: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, -) -> Pubkey { - let state_tree_info = rpc.get_random_state_tree_info().unwrap(); - - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new_with_cpi_context( - *program_id, - state_tree_info.cpi_context.unwrap(), - ); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let decimals = 6u8; - let mint_authority_keypair = Keypair::new(); - let mint_authority = mint_authority_keypair.pubkey(); - let freeze_authority = mint_authority; - let mint_signer = Keypair::new(); - let compressed_mint_address = - derive_mint_compressed_address(&mint_signer.pubkey(), &address_tree_pubkey); - - let (spl_mint, mint_bump) = find_mint_address(&mint_signer.pubkey()); - let accounts = csdk_anchor_derived_test::accounts::CreateUserRecordAndGameSession { - user: user.pubkey(), - user_record: *user_record_pda, - game_session: *game_session_pda, - mint_signer: mint_signer.pubkey(), - ctoken_program: LIGHT_TOKEN_PROGRAM_ID.into(), - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_sponsor: RENT_SPONSOR, - mint_authority, - compress_token_program_cpi_authority: CPI_AUTHORITY_PDA.into(), - }; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![ - AddressWithTree { - address: user_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: game_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: compressed_mint_address, - tree: address_tree_pubkey, - }, - ], - None, - ) - .await - .unwrap() - .value; - - let user_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let game_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let _mint_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let user_address_tree_info = packed_tree_infos.address_trees[0]; - let game_address_tree_info = packed_tree_infos.address_trees[1]; - let mint_address_tree_info = packed_tree_infos.address_trees[2]; - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = csdk_anchor_derived_test::instruction::CreateUserRecordAndGameSession { - account_data: AccountCreationData { - user_name: "Combined User".to_string(), - session_id, - game_type: "Test Game".to_string(), - mint_name: "Test Game Token".to_string(), - mint_symbol: "TGT".to_string(), - mint_uri: "https://example.com/token.json".to_string(), - mint_decimals: 9, - mint_supply: 1_000_000_000, - mint_update_authority: Some(mint_authority), - mint_freeze_authority: Some(freeze_authority), - additional_metadata: None, - }, - compression_params: CompressionParams { - proof: rpc_result.proof, - user_compressed_address, - user_address_tree_info, - user_output_state_tree_index, - game_compressed_address, - game_address_tree_info, - game_output_state_tree_index, - mint_bump, - mint_with_context: CompressedMintWithContext { - leaf_index: 0, - prove_by_index: false, - root_index: mint_address_tree_info.root_index, - address: compressed_mint_address, - mint: Some(CompressedMintInstructionData { - supply: 0, - decimals, - metadata: CompressedMintMetadata { - version: 3, - mint: spl_mint.into(), - cmint_decompressed: false, - mint_signer: mint_signer.pubkey().to_bytes(), - bump: mint_bump, - }, - mint_authority: Some(mint_authority.into()), - freeze_authority: Some(freeze_authority.into()), - extensions: None, - }), - }, - }, - }; - - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction( - &[instruction], - &user.pubkey(), - &[user, &mint_signer, &mint_authority_keypair], - ) - .await; - - assert!( - result.is_ok(), - "Combined creation transaction should succeed: {:?}", - result - ); - - mint_signer.pubkey() -} diff --git a/sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md b/sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md new file mode 100644 index 0000000000..24eff7c227 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md @@ -0,0 +1,198 @@ +# Light Protocol: Atomic PDA + Mint Creation via Macros + +## Overview + +This test program demonstrates how `#[compressible]` PDAs and `#[light_mint]` can be combined in a single instruction with a single proof, enabling atomic creation of compressed accounts and decompressed mints. + +## Key Components + +### 1. Macro Attributes + +**`#[compressible]`** - Applied to PDA account fields: +- `address_tree_info`: Packed address tree info from params +- `output_tree`: State tree index for compressed account output + +**`#[light_mint]`** - Applied to mint placeholder fields: +- `mint_signer`: PDA that derives the CMint address +- `authority`: Mint authority (must be signer) +- `decimals`: Mint decimals +- `address_tree_info`: Address tree info for mint's compressed address +- `signer_seeds`: Optional seeds for PDA signing + +### 2. Derive Macros + +**`#[derive(LightFinalize)]`** - Implements `LightPreInit` and `LightFinalize` traits: +- Detects `#[compressible]` and `#[light_mint]` fields +- Auto-detects ctoken accounts: `ctoken_compressible_config`, `ctoken_rent_sponsor`, `ctoken_program`, `ctoken_cpi_authority` + +**`#[light_instruction(params)]`** - Wraps instruction handlers: +- Calls `light_pre_init()` BEFORE instruction body (all compression logic here) +- Calls `light_finalize()` AFTER instruction body (no-op) + +### 3. Execution Flow (PDAs + Mint) + +``` +Instruction Entry + | + v +light_pre_init() + | + +---> 1. Build CpiAccounts with CPI context + | + +---> 2. Prepare compressed account infos for all PDAs (with_data=false) + | + +---> 3. write_to_cpi_context_first() - Write PDAs to CPI context + | + +---> 4. Build MintActionCompressedInstructionData + | - CreateMint with compressed address + | - DecompressMintAction (creates CMint on-chain) + | - CpiContext config (set_context: false, reads existing) + | + +---> 5. Build MintActionMetaConfig with compressible_cmint + | + +---> 6. invoke/invoke_signed to ctoken program + | - Creates CMint PDA on-chain (DECOMPRESSED/"HOT") + | - Registers mint's compressed address + | - Light System reads PDAs from CPI context + | - All addresses registered atomically + | + v + Return Ok(true) + | + v +Instruction Body + (Can use HOT CMint: mintTo, burn, transfer, etc.) + | + v +light_finalize() -> Ok(()) [no-op] + | + v +Anchor Exit (serializes all account data) +``` + +### 4. Key Design Decisions + +**All compression in pre_init**: +- CMint is created and decompressed BEFORE instruction body runs +- Instruction body can immediately use the HOT mint (mintTo, burn, etc.) +- This enables patterns like `raydium-cp-swap` where mint operations follow creation + +**with_data=false for PDAs**: +- Compressed account only gets the address (no data hash) +- Actual data stays on-chain PDA with CompressionInfo +- Later auto-compression will fully compress and close the PDA +- SDK enforces this: `with_data=true` throws "not supported yet" + +**CPI Context Batching**: When PDAs and mints are combined: +1. PDAs are written to CPI context first via `write_to_cpi_context_first()` +2. Mint action reads from the same CPI context (set_context: false) +3. Light System processes all operations atomically + +**Tree Indexing**: Critical for CPI context validation: +- `in_tree_index` is 1-indexed (Light System does `in_tree_index - 1`) +- Points to the state queue, which has `associated_merkle_tree` +- Must match the CPI context's `associated_merkle_tree` + +### 5. Required Accounts for Combined Flow + +```rust +pub struct CreatePdasAndMintAuto<'info> { + pub fee_payer: Signer<'info>, + pub authority: Signer<'info>, + pub mint_authority: Signer<'info>, + pub mint_signer: UncheckedAccount<'info>, // CMint derives from this + + #[compressible(...)] + pub user_record: Account<'info, UserRecord>, // PDA to compress + + #[compressible(...)] + pub game_session: Account<'info, GameSession>, // Another PDA + + #[light_mint(...)] + pub lp_mint: UncheckedAccount<'info>, // CMint placeholder (HOT after pre_init) + + pub vault: UncheckedAccount<'info>, // Program-owned CToken vault + pub vault_authority: UncheckedAccount<'info>, // Vault owner PDA + pub user_ata: UncheckedAccount<'info>, // User's ATA for lp_mint + + pub compression_config: AccountInfo<'info>, // Light protocol config + pub ctoken_compressible_config: AccountInfo<'info>, // Ctoken config + pub ctoken_rent_sponsor: AccountInfo<'info>, // Rent sponsor + pub ctoken_program: AccountInfo<'info>, // Ctoken program + pub ctoken_cpi_authority: AccountInfo<'info>, // Ctoken CPI authority + pub system_program: Program<'info, System>, +} +``` + +### 6. Instruction Body: Using the HOT CMint + +After `light_pre_init()` creates and decompresses the CMint, the instruction body can immediately use it: + +```rust +#[light_instruction(params)] +pub fn create_pdas_and_mint_auto<'info>(ctx: ..., params: ...) -> Result<()> { + // 1. Populate PDA data (compression handled by macro) + ctx.accounts.user_record.owner = params.owner; + ctx.accounts.game_session.session_id = params.session_id; + + // 2. Create program-owned CToken vault (like cp-swap's token vaults) + CreateCTokenAccountCpi { + payer: ctx.accounts.fee_payer.to_account_info(), + account: ctx.accounts.vault.to_account_info(), + mint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint from pre_init + owner: ctx.accounts.vault_authority.key(), + compressible: CompressibleParamsCpi { ... }, + }.invoke_signed(&[vault_seeds])?; + + // 3. Create user's ATA (like cp-swap's creator_lp_token) + CreateAssociatedCTokenAccountCpi { + owner: ctx.accounts.fee_payer.to_account_info(), + mint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint + associated_token_account: ctx.accounts.user_ata.to_account_info(), + compressible: CompressibleParamsCpi { ... }, + }.invoke()?; + + // 4. Mint tokens to vault and user's ATA + CTokenMintToCpi { + cmint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint + destination: ctx.accounts.vault.to_account_info(), + amount: params.vault_mint_amount, + authority: ctx.accounts.mint_authority.to_account_info(), + }.invoke()?; + + CTokenMintToCpi { + cmint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint + destination: ctx.accounts.user_ata.to_account_info(), + amount: params.user_ata_mint_amount, + authority: ctx.accounts.mint_authority.to_account_info(), + }.invoke()?; + + Ok(()) +} +``` + +### 7. Test: `test_create_pdas_and_mint_auto` + +Demonstrates the full cp-swap-like flow: +1. Setup compression config and signers +2. Derive PDAs, CMint, vault, and user_ata addresses +3. Get validity proof for all 3 compressed addresses (2 PDAs + 1 mint) +4. Build instruction with CPI context enabled +5. Execute single transaction +6. Verify: + - 2 PDAs compressed (address only, data on-chain) + - 1 CMint created and decompressed (HOT) + - 1 Program-owned vault with correct balance (e.g., 100 tokens) + - 1 User ATA with correct balance (e.g., 50 tokens) + - Both vault and ATA owned by ctoken program + +## Conclusion + +The macro system enables atomic creation of an arbitrary combination of compressed PDAs and decompressed mints in a single instruction with a single proof. All compression logic runs in `light_pre_init()`, so the instruction body can immediately use the HOT CMint for operations like `mintTo`, `burn`, and `transfer`. This pattern is essential for programs like `raydium-cp-swap` where multiple accounts (pool state, observation state, LP mint, token vaults, user ATAs) must be created and operated on atomically. + +**The full flow in one instruction:** +1. `pre_init()`: Compress 2 PDAs + Create+Decompress CMint (atomically) +2. `instruction body`: Create vault + Create user_ata + MintTo both +3. `finalize()`: no-op + +All accounts (PDAs, CMint, vault, user_ata) exist and are usable within the same instruction. 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 1e1db1be0d..08be08c4b6 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 @@ -1,46 +1,82 @@ use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; use crate::state::*; -#[derive(Accounts)] -#[instruction(account_data: AccountCreationData)] -pub struct CreateUserRecordAndGameSession<'info> { +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct FullAutoWithMintParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, + pub category_id: u64, + pub session_id: u64, + pub mint_signer_bump: u8, + pub vault_bump: u8, + pub user_ata_bump: u8, + pub vault_mint_amount: u64, + pub user_ata_mint_amount: u64, +} + +pub const LP_MINT_SIGNER_SEED: &[u8] = b"lp_mint_signer"; +pub const AUTO_VAULT_SEED: &[u8] = b"auto_vault"; +pub const AUTO_VAULT_AUTHORITY_SEED: &[u8] = b"auto_vault_authority"; + +#[derive(Accounts, RentFree)] +#[instruction(params: FullAutoWithMintParams)] +pub struct CreatePdasAndMintAuto<'info> { #[account(mut)] - pub user: Signer<'info>, + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, - /// The mint signer used for PDA derivation - pub mint_signer: Signer<'info>, + #[account(mut)] + pub mint_authority: Signer<'info>, + + /// CHECK: PDA derived from authority + #[account( + seeds = [LP_MINT_SIGNER_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer: UncheckedAccount<'info>, #[account( init, - payer = user, - // Space: discriminator(8) + owner(32) + name_len(4) + name(32) + score(8) + category_id(8) = 92 bytes - space = 8 + 32 + 4 + 32 + 8 + 8, + payer = fee_payer, + space = 8 + UserRecord::INIT_SPACE, seeds = [ b"user_record", authority.key().as_ref(), mint_authority.key().as_ref(), - account_data.owner.as_ref(), - account_data.category_id.to_le_bytes().as_ref() + params.owner.as_ref(), + params.category_id.to_le_bytes().as_ref() ], bump, )] + #[rentfree] pub user_record: Account<'info, UserRecord>, + #[account( init, +<<<<<<< HEAD payer = user, // Space: discriminator(8) + session_id(8) + player(32) + game_type_len(4) + // game_type(32) + start_time(8) + end_time(1+8) + score(8) = 109 bytes space = 8 + 8 + 32 + 4 + 32 + 8 + 9 + 8, +======= + payer = fee_payer, + space = 8 + GameSession::INIT_SPACE, +>>>>>>> a606eb113 (wip) seeds = [ b"game_session", - crate::max_key(&user.key(), &authority.key()).as_ref(), - account_data.session_id.to_le_bytes().as_ref() + crate::max_key(&fee_payer.key(), &authority.key()).as_ref(), + params.session_id.to_le_bytes().as_ref() ], bump, )] + #[rentfree] pub game_session: Account<'info, GameSession>, +<<<<<<< HEAD /// Authority signer used in PDA seeds pub authority: Signer<'info>, @@ -67,6 +103,52 @@ pub struct CreateUserRecordAndGameSession<'info> { /// Rent recipient - must match config /// CHECK: Rent recipient is validated against the config +======= + /// CHECK: Initialized by mint_action +>>>>>>> a606eb113 (wip) + #[account(mut)] + #[light_mint( + mint_signer = mint_signer, + authority = mint_authority, + decimals = 9, + signer_seeds = &[LP_MINT_SIGNER_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_bump]] + )] + pub cmint: UncheckedAccount<'info>, + + /// CHECK: Initialized via CToken CPI + #[account( + mut, + seeds = [VAULT_SEED, cmint.key().as_ref()], + bump, + )] + #[rentfree_token(Vault, authority = [b"vault_authority"])] + pub vault: UncheckedAccount<'info>, + + /// CHECK: PDA used as vault owner + #[account(seeds = [b"vault_authority"], bump)] + pub vault_authority: UncheckedAccount<'info>, + + /// CHECK: Initialized via CToken CPI #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, + pub user_ata: UncheckedAccount<'info>, + + /// 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 ctoken_program: AccountInfo<'info>, + + /// CHECK: CToken CPI authority + pub ctoken_cpi_authority: AccountInfo<'info>, + + pub system_program: Program<'info, System>, } + +pub const VAULT_SEED: &[u8] = b"vault"; 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 08563dc6b1..8dd1e1b545 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use light_sdk::{derive_light_cpi_signer, derive_light_rent_sponsor_pda}; -use light_sdk_macros::add_compressible_instructions; +use light_sdk_macros::{light_instruction, rentfree_program}; use light_sdk_types::CpiSigner; pub mod errors; @@ -10,12 +10,8 @@ pub mod instruction_accounts; pub mod state; pub use instruction_accounts::*; -pub use state::{ - AccountCreationData, CompressionParams, GameSession, PackedGameSession, - PackedPlaceholderRecord, PackedUserRecord, PlaceholderRecord, UserRecord, -}; +pub use state::{GameSession, PackedGameSession, PackedUserRecord, PlaceholderRecord, UserRecord}; -// Example helper expression usable in seeds #[inline] pub fn max_key(left: &Pubkey, right: &Pubkey) -> [u8; 32] { if left > right { @@ -30,16 +26,15 @@ declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); -/// Derive a program-owned rent sponsor PDA (version = 1 by default). pub const PROGRAM_RENT_SPONSOR_DATA: ([u8; 32], u8) = derive_light_rent_sponsor_pda!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah", 1); -/// Returns the program's rent sponsor PDA as a Pubkey. #[inline] pub fn program_rent_sponsor() -> Pubkey { Pubkey::from(PROGRAM_RENT_SPONSOR_DATA.0) } +<<<<<<< HEAD #[add_compressible_instructions( // Complex PDA account types with seed specifications using BOTH ctx.accounts.* AND data.* // UserRecord: uses ctx accounts (authority, mint_authority) + data fields (owner, category_id) @@ -80,42 +75,47 @@ pub mod csdk_anchor_full_derived_test { use light_token_sdk::compressed_token::{ create_compressed_mint::find_mint_address, mint_action::MintActionMetaConfig, }; +======= +pub const GAME_SESSION_SEED: &str = "game_session"; + +#[rentfree_program] +#[program] +pub mod csdk_anchor_full_derived_test { + #![allow(clippy::too_many_arguments)] +>>>>>>> a606eb113 (wip) use super::*; use crate::{ - errors::ErrorCode, + instruction_accounts::CreatePdasAndMintAuto, state::{GameSession, UserRecord}, - LIGHT_CPI_SIGNER, + FullAutoWithMintParams, LIGHT_CPI_SIGNER, }; - pub fn create_user_record_and_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, - account_data: AccountCreationData, - compression_params: CompressionParams, + #[light_instruction] + pub fn create_pdas_and_mint_auto<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePdasAndMintAuto<'info>>, + params: FullAutoWithMintParams, ) -> Result<()> { - let user_record = &mut ctx.accounts.user_record; - let game_session = &mut ctx.accounts.game_session; - - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ErrorCode::RentRecipientMismatch.into()); - } + use anchor_lang::solana_program::sysvar::clock::Clock; + use light_ctoken_sdk::ctoken::{ + CTokenMintToCpi, CreateCTokenAccountCpi, CreateCTokenAtaCpi, + }; - // Populate UserRecord - user_record.owner = account_data.owner; - user_record.name = account_data.user_name.clone(); - user_record.score = 11; - user_record.category_id = account_data.category_id; + let user_record = &mut ctx.accounts.user_record; + user_record.owner = params.owner; + user_record.name = "Auto Created User With Mint".to_string(); + user_record.score = 0; + user_record.category_id = params.category_id; - // Populate GameSession - game_session.session_id = account_data.session_id; - game_session.player = ctx.accounts.user.key(); - game_session.game_type = account_data.game_type.clone(); + let game_session = &mut ctx.accounts.game_session; + game_session.session_id = params.session_id; + game_session.player = ctx.accounts.fee_payer.key(); + game_session.game_type = "Auto Game With Mint".to_string(); game_session.start_time = Clock::get()?.unix_timestamp as u64; game_session.end_time = None; game_session.score = 0; +<<<<<<< HEAD let cpi_accounts = CpiAccounts::new_with_config( ctx.accounts.user.as_ref(), ctx.remaining_accounts, @@ -207,19 +207,55 @@ pub mod csdk_anchor_full_derived_test { read_only_address_trees: [0; 4], }, ); - - // Build account metas - let mut config = MintActionMetaConfig::new_create_mint( - ctx.accounts.user.key(), // fee_payer - ctx.accounts.mint_authority.key(), // authority (mint authority) - ctx.accounts.mint_signer.key(), // mint_signer - address_tree_pubkey, - output_queue, +======= + let cmint_key = ctx.accounts.cmint.key(); + CreateCTokenAccountCpi { + payer: ctx.accounts.fee_payer.to_account_info(), + account: ctx.accounts.vault.to_account_info(), + mint: ctx.accounts.cmint.to_account_info(), + owner: ctx.accounts.vault_authority.key(), + } + .rent_free( + ctx.accounts.ctoken_compressible_config.to_account_info(), + ctx.accounts.ctoken_rent_sponsor.to_account_info(), + ctx.accounts.system_program.to_account_info(), + &crate::ID, ) - .with_mint_compressed_tokens(); - - config.cpi_context = Some(cpi_context_pubkey); + .invoke_signed(&[ + crate::instruction_accounts::VAULT_SEED, + cmint_key.as_ref(), + &[params.vault_bump], + ])?; +>>>>>>> a606eb113 (wip) + + CreateCTokenAtaCpi { + payer: ctx.accounts.fee_payer.to_account_info(), + owner: ctx.accounts.fee_payer.to_account_info(), + mint: ctx.accounts.cmint.to_account_info(), + ata: ctx.accounts.user_ata.to_account_info(), + bump: params.user_ata_bump, + } + .idempotent() + .rent_free( + ctx.accounts.ctoken_compressible_config.to_account_info(), + ctx.accounts.ctoken_rent_sponsor.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ) + .invoke()?; + + if params.vault_mint_amount > 0 { + CTokenMintToCpi { + cmint: ctx.accounts.cmint.to_account_info(), + destination: ctx.accounts.vault.to_account_info(), + amount: params.vault_mint_amount, + authority: ctx.accounts.mint_authority.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + max_top_up: None, + } + .invoke()?; + } +<<<<<<< HEAD let account_metas = config.to_account_metas(); // Serialize instruction data @@ -247,6 +283,19 @@ pub mod csdk_anchor_full_derived_test { user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; +======= + if params.user_ata_mint_amount > 0 { + CTokenMintToCpi { + cmint: ctx.accounts.cmint.to_account_info(), + destination: ctx.accounts.user_ata.to_account_info(), + amount: params.user_ata_mint_amount, + authority: ctx.accounts.mint_authority.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + max_top_up: None, + } + .invoke()?; + } +>>>>>>> a606eb113 (wip) Ok(()) } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state.rs index 27ce2224eb..91595058f6 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state.rs @@ -1,4 +1,5 @@ use anchor_lang::prelude::*; +<<<<<<< HEAD use light_sdk::{ compressible::CompressionInfo, instruction::{PackedAddressTreeInfo, ValidityProof}, @@ -6,15 +7,15 @@ use light_sdk::{ }; use light_sdk_macros::{Compressible, CompressiblePack}; use light_token_interface::instructions::mint_action::CompressedMintWithContext; +======= +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::Light; +>>>>>>> a606eb113 (wip) -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] +#[derive(Default, Debug, InitSpace, Light)] #[account] pub struct UserRecord { - #[skip] pub compression_info: Option, - #[hash] pub owner: Pubkey, #[max_len(32)] pub name: String, @@ -22,16 +23,12 @@ pub struct UserRecord { pub category_id: u64, } -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] +#[derive(Default, Debug, InitSpace, Light)] #[compress_as(start_time = 0, end_time = None, score = 0)] #[account] pub struct GameSession { - #[skip] pub compression_info: Option, pub session_id: u64, - #[hash] pub player: Pubkey, #[max_len(32)] pub game_type: String, @@ -40,56 +37,13 @@ pub struct GameSession { pub score: u64, } -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] +#[derive(Default, Debug, InitSpace, Light)] #[account] pub struct PlaceholderRecord { - #[skip] pub compression_info: Option, - #[hash] pub owner: Pubkey, #[max_len(32)] pub name: String, pub placeholder_id: u64, pub counter: u32, } - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct AccountCreationData { - // Instruction data fields (accounts come from ctx.accounts.*) - pub owner: Pubkey, - pub category_id: u64, - pub user_name: String, - pub session_id: u64, - pub game_type: String, - pub placeholder_id: u64, - pub counter: u32, - pub mint_name: String, - pub mint_symbol: String, - pub mint_uri: String, - pub mint_decimals: u8, - pub mint_supply: u64, - pub mint_update_authority: Option, - pub mint_freeze_authority: Option, - pub additional_metadata: Option>, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct TokenAccountInfo { - pub user: Pubkey, - pub mint: Pubkey, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct CompressionParams { - pub proof: ValidityProof, - pub user_compressed_address: [u8; 32], - pub user_address_tree_info: PackedAddressTreeInfo, - pub user_output_state_tree_index: u8, - pub game_compressed_address: [u8; 32], - pub game_address_tree_info: PackedAddressTreeInfo, - pub game_output_state_tree_index: u8, - pub mint_bump: u8, - pub mint_with_context: CompressedMintWithContext, -} 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 fbe723e9a6..8819784400 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 @@ -1,16 +1,21 @@ +<<<<<<< HEAD use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; use csdk_anchor_full_derived_test::{ AccountCreationData, CompressionParams, GameSession, UserRecord, }; use light_compressed_account::address::derive_address; +======= +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_compressible_client::{ + get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, +}; +use light_ctoken_sdk::compressed_token::create_compressed_mint::find_cmint_address; +>>>>>>> a606eb113 (wip) use light_macros::pubkey; use light_program_test::{ - program_test::{setup_mock_program_data, LightProgramTest}, - AddressWithTree, Indexer, ProgramTestConfig, Rpc, -}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, + program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, + Indexer, ProgramTestConfig, Rpc, }; use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; use light_token_interface::{ @@ -26,11 +31,56 @@ use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signer::Signer; -const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx")]; const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +/// 2 PDAs + 1 CMint + 1 Vault + 1 User ATA, all in one instruction with single proof. +/// After init: all accounts on-chain + parseable. +/// After warp: all cold (auto-compressed) with non-empty compressed data. #[tokio::test] -async fn test_create_with_complex_seeds() { +async fn test_create_pdas_and_mint_auto() { + use csdk_anchor_full_derived_test::instruction_accounts::{LP_MINT_SIGNER_SEED, VAULT_SEED}; + use csdk_anchor_full_derived_test::FullAutoWithMintParams; + use light_ctoken_sdk::ctoken::{ + get_associated_ctoken_address_and_bump, CToken, COMPRESSIBLE_CONFIG_V1, + RENT_SPONSOR as CTOKEN_RENT_SPONSOR, + }; + + // Helpers + 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); + } + fn parse_ctoken(data: &[u8]) -> CToken { + borsh::BorshDeserialize::deserialize(&mut &data[..]).unwrap() + } + 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()); + } + async fn assert_compressed_token_exists( + rpc: &mut LightProgramTest, + owner: &Pubkey, + expected_amount: u64, + ) { + let accs = rpc + .get_compressed_token_accounts_by_owner(owner, None, None) + .await + .unwrap() + .value + .items; + assert!(!accs.is_empty()); + assert_eq!(accs[0].token.amount, expected_amount); + } + let program_id = csdk_anchor_full_derived_test::ID; let mut config = ProgramTestConfig::new_v2( true, @@ -41,130 +91,215 @@ async fn test_create_with_complex_seeds() { let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - // Initialize compression config using the macro-generated instruction - let config_instruction = - csdk_anchor_full_derived_test::instruction::InitializeCompressionConfig { - rent_sponsor: RENT_SPONSOR, - compression_authority: payer.pubkey(), - rent_config: light_compressible::rent::RentConfig::default(), - write_top_up: 5_000, - address_space: vec![ADDRESS_SPACE[0]], - }; - let config_accounts = csdk_anchor_full_derived_test::accounts::InitializeCompressionConfig { - payer: payer.pubkey(), - config: config_pda, - program_data: _program_data_pda, - authority: payer.pubkey(), - system_program: solana_sdk::system_program::ID, - }; - let instruction = Instruction { - program_id, - accounts: config_accounts.to_account_metas(None), - data: config_instruction.data(), - }; - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) - .await; - assert!( - result.is_ok(), - "Initialize config should succeed: {:?}", - result - ); + 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"); - // Create additional signers for complex seeds let authority = Keypair::new(); - let mint_authority_keypair = Keypair::new(); - let some_account = Keypair::new(); + let mint_authority = Keypair::new(); - let session_id = 42424u64; - let category_id = 777u64; + let owner = payer.pubkey(); + let category_id = 111u64; + let session_id = 222u64; + let vault_mint_amount = 100u64; + let user_ata_mint_amount = 50u64; - // Calculate PDAs with complex seeds using ctx accounts - let (user_record_pda, _user_record_bump) = Pubkey::find_program_address( + // Derive PDAs + let (mint_signer_pda, mint_signer_bump) = Pubkey::find_program_address( + &[LP_MINT_SIGNER_SEED, authority.pubkey().as_ref()], + &program_id, + ); + let (cmint_pda, _) = find_cmint_address(&mint_signer_pda); + let (vault_pda, vault_bump) = + Pubkey::find_program_address(&[VAULT_SEED, cmint_pda.as_ref()], &program_id); + let (vault_authority_pda, _) = Pubkey::find_program_address(&[b"vault_authority"], &program_id); + let (user_ata_pda, user_ata_bump) = + get_associated_ctoken_address_and_bump(&payer.pubkey(), &cmint_pda); + + let (user_record_pda, _) = Pubkey::find_program_address( &[ b"user_record", authority.pubkey().as_ref(), - mint_authority_keypair.pubkey().as_ref(), - payer.pubkey().as_ref(), // owner from instruction data + mint_authority.pubkey().as_ref(), + owner.as_ref(), category_id.to_le_bytes().as_ref(), ], &program_id, ); - // GameSession uses max_key(ctx.user, ctx.authority) for the seed let max_key_result = csdk_anchor_full_derived_test::max_key(&payer.pubkey(), &authority.pubkey()); - let (game_session_pda, _game_bump) = Pubkey::find_program_address( + let (game_session_pda, _) = Pubkey::find_program_address( &[ - b"game_session", + csdk_anchor_full_derived_test::GAME_SESSION_SEED.as_bytes(), max_key_result.as_ref(), session_id.to_le_bytes().as_ref(), ], &program_id, ); - let mint_signer_pubkey = create_user_record_and_game_session( - &mut rpc, - &payer, + let proof_result = get_create_accounts_proof( + &rpc, &program_id, - &config_pda, - &user_record_pda, - &game_session_pda, - &authority, - &mint_authority_keypair, - &some_account, - session_id, - category_id, + vec![ + CreateAccountsProofInput::pda(user_record_pda), + CreateAccountsProofInput::pda(game_session_pda), + CreateAccountsProofInput::mint(mint_signer_pda), + ], ) - .await; + .await + .unwrap(); + // Derive compressed addresses for later assertions let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let user_compressed_address = derive_address( + let user_compressed_address = light_compressed_account::address::derive_address( &user_record_pda.to_bytes(), &address_tree_pubkey.to_bytes(), &program_id.to_bytes(), ); - let game_compressed_address = derive_address( + let game_compressed_address = light_compressed_account::address::derive_address( &game_session_pda.to_bytes(), &address_tree_pubkey.to_bytes(), &program_id.to_bytes(), ); + let mint_compressed_address = + light_ctoken_sdk::compressed_token::create_compressed_mint::derive_cmint_compressed_address( + &mint_signer_pda, + &address_tree_pubkey, + ); + + let accounts = csdk_anchor_full_derived_test::accounts::CreatePdasAndMintAuto { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_authority: mint_authority.pubkey(), + mint_signer: mint_signer_pda, + user_record: user_record_pda, + game_session: game_session_pda, + cmint: cmint_pda, + vault: vault_pda, + vault_authority: vault_authority_pda, + user_ata: user_ata_pda, + compression_config: config_pda, + ctoken_compressible_config: COMPRESSIBLE_CONFIG_V1, + ctoken_rent_sponsor: CTOKEN_RENT_SPONSOR, + ctoken_program: C_TOKEN_PROGRAM_ID.into(), + ctoken_cpi_authority: light_ctoken_types::CPI_AUTHORITY_PDA.into(), + system_program: solana_sdk::system_program::ID, + }; - // Verify compressed user record - let compressed_user_record = rpc - .get_compressed_account(user_compressed_address, None) + // Simplified instruction data - just pass create_accounts_proof directly + let instruction_data = csdk_anchor_full_derived_test::instruction::CreatePdasAndMintAuto { + params: FullAutoWithMintParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + category_id, + session_id, + mint_signer_bump, + vault_bump, + user_ata_bump, + vault_mint_amount, + user_ata_mint_amount, + }, + }; + + 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, &mint_authority], + ) + .await + .unwrap(); + + // PHASE 1: After init - all accounts on-chain and parseable + assert_onchain_exists(&mut rpc, &user_record_pda).await; + assert_onchain_exists(&mut rpc, &game_session_pda).await; + assert_onchain_exists(&mut rpc, &cmint_pda).await; + assert_onchain_exists(&mut rpc, &vault_pda).await; + assert_onchain_exists(&mut rpc, &user_ata_pda).await; + + // Parse and verify CToken data + let vault_data = parse_ctoken(&rpc.get_account(vault_pda).await.unwrap().unwrap().data); + assert_eq!(vault_data.owner, vault_authority_pda.to_bytes()); + assert_eq!(vault_data.amount, vault_mint_amount); + + let user_ata_data = parse_ctoken(&rpc.get_account(user_ata_pda).await.unwrap().unwrap().data); + assert_eq!(user_ata_data.owner, payer.pubkey().to_bytes()); + assert_eq!(user_ata_data.amount, user_ata_mint_amount); + + // Verify compressed addresses registered (empty data - decompressed to on-chain) + let compressed_cmint = rpc + .get_compressed_account(mint_compressed_address, None) .await .unwrap() .value .unwrap(); + assert_eq!(compressed_cmint.address.unwrap(), mint_compressed_address); + assert!(compressed_cmint.data.as_ref().unwrap().data.is_empty()); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + + // After warp: all on-chain accounts should be closed + assert_onchain_closed(&mut rpc, &user_record_pda).await; + assert_onchain_closed(&mut rpc, &game_session_pda).await; + assert_onchain_closed(&mut rpc, &cmint_pda).await; + assert_onchain_closed(&mut rpc, &vault_pda).await; + assert_onchain_closed(&mut rpc, &user_ata_pda).await; + + // Compressed accounts should exist with non-empty data + assert_compressed_exists_with_data(&mut rpc, user_compressed_address).await; + assert_compressed_exists_with_data(&mut rpc, game_compressed_address).await; + assert_compressed_exists_with_data(&mut rpc, mint_compressed_address).await; + + // Compressed token accounts should exist with correct balances + assert_compressed_token_exists(&mut rpc, &vault_pda, vault_mint_amount).await; + assert_compressed_token_exists(&mut rpc, &user_ata_pda, user_ata_mint_amount).await; + + // PHASE 3: Decompress PDAs + vault via build_decompress_idempotent + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ + CTokenAccountVariant, GameSessionSeeds, UserRecordSeeds, + }; + use light_compressible_client::{ + compressible_instruction, AccountInterface, RentFreeDecompressAccount, + }; - assert_eq!( - compressed_user_record.address, - Some(user_compressed_address) - ); - assert!(compressed_user_record.data.is_some()); - - let user_buf = compressed_user_record.data.unwrap().data; - let user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); - - assert_eq!(user_record.name, "Complex User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, payer.pubkey()); - assert_eq!(user_record.category_id, category_id); - assert!(user_record.compression_info.is_none()); + // Fetch compressed PDA accounts + let compressed_user = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); - // Verify compressed game session - let compressed_game_session = rpc + let compressed_game = rpc .get_compressed_account(game_compressed_address, None) .await .unwrap() .value .unwrap(); +<<<<<<< HEAD assert_eq!( compressed_game_session.address, Some(game_compressed_address) @@ -254,62 +389,52 @@ pub async fn create_user_record_and_game_session( &address_tree_pubkey.to_bytes(), &program_id.to_bytes(), ); +======= + // Fetch compressed vault token account + let compressed_vault_accounts = rpc + .get_compressed_token_accounts_by_owner(&vault_pda, None, None) + .await + .unwrap() + .value + .items; + let compressed_vault = &compressed_vault_accounts[0]; +>>>>>>> a606eb113 (wip) + // Get validity proof for PDAs + vault let rpc_result = rpc .get_validity_proof( - vec![], vec![ - AddressWithTree { - address: user_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: game_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: compressed_mint_address, - tree: address_tree_pubkey, - }, + compressed_user.hash, + compressed_game.hash, + compressed_vault.account.hash, ], + vec![], None, ) .await .unwrap() .value; - let user_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let game_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let _mint_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let user_address_tree_info = packed_tree_infos.address_trees[0]; - let game_address_tree_info = packed_tree_infos.address_trees[1]; - let mint_address_tree_info = packed_tree_infos.address_trees[2]; - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = - csdk_anchor_full_derived_test::instruction::CreateUserRecordAndGameSession { - account_data: AccountCreationData { - // Instruction data fields (accounts come from ctx.accounts.*) - owner: user.pubkey(), + // Build RentFreeDecompressAccount using from_seeds and from_ctoken helpers + let decompress_accounts = vec![ + RentFreeDecompressAccount::from_seeds( + AccountInterface::cold(user_record_pda, compressed_user.clone()), + UserRecordSeeds { + authority: authority.pubkey(), + mint_authority: mint_authority.pubkey(), + owner, category_id, - user_name: "Complex User".to_string(), + }, + ) + .expect("UserRecord seed verification failed"), + RentFreeDecompressAccount::from_seeds( + AccountInterface::cold(game_session_pda, compressed_game.clone()), + GameSessionSeeds { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), session_id, - game_type: "Complex Game".to_string(), - placeholder_id: 0, - counter: 0, - mint_name: "Complex Token".to_string(), - mint_symbol: "CPLX".to_string(), - mint_uri: "https://example.com/complex.json".to_string(), - mint_decimals: 9, - mint_supply: 1_000_000_000, - mint_update_authority: Some(mint_authority), - mint_freeze_authority: Some(freeze_authority), - additional_metadata: None, }, +<<<<<<< HEAD compression_params: CompressionParams { proof: rpc_result.proof, user_compressed_address, @@ -341,26 +466,166 @@ pub async fn create_user_record_and_game_session( }, }, }; +======= + ) + .expect("GameSession seed verification failed"), + RentFreeDecompressAccount::from_ctoken( + AccountInterface::cold(vault_pda, compressed_vault.account.clone()), + CTokenAccountVariant::Vault { cmint: cmint_pda }, + ) + .expect("CToken variant construction failed"), + ]; +>>>>>>> a606eb113 (wip) - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), + // Build decompress instruction + // No SeedParams needed - data.* seeds from unpacked account, ctx.* from variant idx + let decompress_instruction = compressible_instruction::build_decompress_idempotent( + &program_id, + decompress_accounts, + compressible_instruction::decompress::accounts(payer.pubkey(), config_pda, payer.pubkey()), + rpc_result, + ) + .unwrap() + .expect("Should have cold accounts to decompress"); + + rpc.create_and_send_transaction(&[decompress_instruction], &payer.pubkey(), &[&payer]) + .await + .expect("PDA + vault decompression should succeed"); + + // Assert PDAs are back on-chain + assert_onchain_exists(&mut rpc, &user_record_pda).await; + assert_onchain_exists(&mut rpc, &game_session_pda).await; + + // Assert vault is back on-chain with correct balance + assert_onchain_exists(&mut rpc, &vault_pda).await; + let vault_after = parse_ctoken(&rpc.get_account(vault_pda).await.unwrap().unwrap().data); + assert_eq!(vault_after.amount, vault_mint_amount); + + // Verify compressed vault token is consumed (no more compressed token accounts for vault) + let remaining_vault = rpc + .get_compressed_token_accounts_by_owner(&vault_pda, None, None) + .await + .unwrap() + .value + .items; + assert!(remaining_vault.is_empty()); + + // PHASE 4: Decompress user ATA via new high-performance API pattern + use light_compressible_client::{ + build_decompress_token_accounts, decompress_cmint, decompress_token_accounts, + parse_token_account_interface, }; - let result = rpc - .create_and_send_transaction( - &[instruction], - &user.pubkey(), - &[user, &mint_signer, mint_authority_keypair, authority], - ) - .await; + // Step 1: Fetch raw account interface (Account bytes always present) + let account_interface = rpc + .get_ata_account_interface(&cmint_pda, &payer.pubkey()) + .await + .expect("get_ata_account_interface should succeed"); + + // Verify raw bytes are present (even for cold accounts) + assert_eq!(account_interface.account.data.len(), 165); + // Step 2: Parse into TokenAccountInterface (sync, no RPC) + let parsed = parse_token_account_interface(&account_interface) + .expect("parse_token_account_interface should succeed"); + + // Verify it's cold (compressed) + assert!(parsed.is_cold, "ATA should be cold after warp"); assert!( - result.is_ok(), - "Complex seed creation transaction should succeed: {:?}", - result + parsed.decompression_context.is_some(), + "Cold ATA should have decompression_context" ); - mint_signer.pubkey() + // Amount accessible via TokenData + assert_eq!(parsed.amount(), user_ata_mint_amount); + + // Step 3: Get proof and build instructions (sync after proof) + let cold_hash = parsed.hash().expect("Cold ATA should have hash"); + let proof = rpc + .get_validity_proof(vec![cold_hash], vec![], None) + .await + .expect("get_validity_proof should succeed") + .value; + + // Step 4: Build decompress instructions (sync) + let ata_instructions = build_decompress_token_accounts(&[parsed], payer.pubkey(), Some(proof)) + .expect("build_decompress_token_accounts should succeed"); + + assert!(!ata_instructions.is_empty(), "Should have instructions"); + + rpc.create_and_send_transaction(&ata_instructions, &payer.pubkey(), &[&payer]) + .await + .expect("ATA decompression should succeed"); + + // Assert user ATA is back on-chain with correct balance + assert_onchain_exists(&mut rpc, &user_ata_pda).await; + let user_ata_after = parse_ctoken(&rpc.get_account(user_ata_pda).await.unwrap().unwrap().data); + assert_eq!(user_ata_after.amount, user_ata_mint_amount); + + // Verify idempotency: calling again should return empty vec + let account_interface_again = rpc + .get_ata_account_interface(&cmint_pda, &payer.pubkey()) + .await + .expect("get_ata_account_interface should succeed"); + + let parsed_again = parse_token_account_interface(&account_interface_again) + .expect("parse_token_account_interface should succeed"); + + assert!( + !parsed_again.is_cold, + "ATA should be hot after decompression" + ); + assert!( + parsed_again.decompression_context.is_none(), + "Hot ATA should not have decompression_context" + ); + + // Using async wrapper (alternative pattern) + let ata_instructions_again = decompress_token_accounts(&[parsed_again], payer.pubkey(), &rpc) + .await + .expect("decompress_token_accounts should succeed"); + assert!( + ata_instructions_again.is_empty(), + "Should return empty vec when already decompressed" + ); + + // PHASE 5: Decompress CMint via decompress_cmint (lean wrapper) + let mint_interface = rpc + .get_mint_interface(&mint_signer_pda) + .await + .expect("get_mint_interface should succeed"); + + // Verify it's cold (compressed) + assert!(mint_interface.is_cold(), "Mint should be cold after warp"); + + // Decompress using lean wrapper (fetches proof internally) + let mint_instructions = decompress_cmint(&mint_interface, payer.pubkey(), &rpc) + .await + .expect("decompress_cmint should succeed"); + + if !mint_instructions.is_empty() { + rpc.create_and_send_transaction(&mint_instructions, &payer.pubkey(), &[&payer]) + .await + .expect("Mint decompression should succeed"); + } + + // Assert CMint is back on-chain + assert_onchain_exists(&mut rpc, &cmint_pda).await; + + // Verify calling again returns empty vec (idempotent) + let mint_interface_again = rpc + .get_mint_interface(&mint_signer_pda) + .await + .expect("get_mint_interface should succeed"); + assert!( + mint_interface_again.is_hot(), + "Mint should be hot after decompression" + ); + let mint_instructions_again = decompress_cmint(&mint_interface_again, payer.pubkey(), &rpc) + .await + .expect("decompress_cmint should succeed"); + assert!( + mint_instructions_again.is_empty(), + "Should return empty vec when mint already decompressed" + ); } diff --git a/sdk-tests/sdk-compressible-test/Anchor.toml b/sdk-tests/sdk-compressible-test/Anchor.toml deleted file mode 100644 index 7225c40f12..0000000000 --- a/sdk-tests/sdk-compressible-test/Anchor.toml +++ /dev/null @@ -1,19 +0,0 @@ -[toolchain] - -[features] -resolution = true -skip-lint = false - -[programs.localnet] -sdk_compressible_test = "FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah" - -[registry] -url = "https://api.apr.dev" - -[provider] -cluster = "Localnet" -wallet = "~/.config/solana/id.json" - -[scripts] -test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" - diff --git a/sdk-tests/sdk-compressible-test/Cargo.toml b/sdk-tests/sdk-compressible-test/Cargo.toml deleted file mode 100644 index ccb30f3186..0000000000 --- a/sdk-tests/sdk-compressible-test/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "sdk-compressible-test" -version = "0.1.0" -description = "Simple Anchor program template with user records" -edition = "2021" - -[lib] -crate-type = ["cdylib", "lib"] -name = "sdk_compressible_test" - -[features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] -test-sbf = [] - -[dependencies] -light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } -light-hasher = { workspace = true, features = ["solana"] } -solana-program = { workspace = true } -solana-system-interface = { workspace = true } -light-macros = { workspace = true, features = ["solana"] } -borsh = { workspace = true } -light-compressed-account = { workspace = true, features = ["solana"] } -anchor-lang = { workspace = true, features = ["idl-build"] } -anchor-spl = { version = "=0.31.1", git = "https://github.com/lightprotocol/anchor", rev = "d8a2b3d9", features = ["memo", "metadata", "idl-build"] } -light-token-interface = { workspace = true, features = ["anchor"] } -light-token-sdk = { workspace = true, features = ["anchor", "compressible"] } -light-token-types = { workspace = true, features = ["anchor"] } -light-compressible = { workspace = true, features = ["anchor"] } - -[dev-dependencies] -light-token-client = { workspace = true } -light-program-test = { workspace = true, features = ["devenv", "v2"] } -light-client = { workspace = true, features = ["v2"] } -light-compressible-client = { workspace = true, features = ["anchor"] } -light-test-utils = { workspace = true} -tokio = { workspace = true } -solana-sdk = { workspace = true } -solana-logger = { workspace = true } -solana-instruction = { workspace = true } -solana-pubkey = { workspace = true } -solana-signature = { workspace = true } -solana-signer = { workspace = true } -solana-keypair = { workspace = true } -solana-account = { workspace = true } -bincode = "1.3" - -[lints.rust.unexpected_cfgs] -level = "allow" -check-cfg = [ - 'cfg(target_os, values("solana"))', - 'cfg(feature, values("frozen-abi", "no-entrypoint"))', -] diff --git a/sdk-tests/sdk-compressible-test/Xargo.toml b/sdk-tests/sdk-compressible-test/Xargo.toml deleted file mode 100644 index 1744f098ae..0000000000 --- a/sdk-tests/sdk-compressible-test/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] \ No newline at end of file diff --git a/sdk-tests/sdk-compressible-test/package.json b/sdk-tests/sdk-compressible-test/package.json deleted file mode 100644 index 3711173d19..0000000000 --- a/sdk-tests/sdk-compressible-test/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@lightprotocol/sdk-compressible-test", - "version": "0.1.0", - "license": "Apache-2.0", - "scripts": { - "build": "cargo build-sbf", - "test": "cargo test-sbf -p sdk-compressible-test -- --nocapture" - }, - "nx": {} -} - diff --git a/sdk-tests/sdk-compressible-test/src/constants.rs b/sdk-tests/sdk-compressible-test/src/constants.rs deleted file mode 100644 index 2fcae0aa3a..0000000000 --- a/sdk-tests/sdk-compressible-test/src/constants.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub const POOL_VAULT_SEED: &str = "pool_vault"; -pub const USER_RECORD_SEED: &str = "user_record"; -pub const CTOKEN_SIGNER_SEED: &str = "ctoken_signer"; diff --git a/sdk-tests/sdk-compressible-test/src/errors.rs b/sdk-tests/sdk-compressible-test/src/errors.rs deleted file mode 100644 index cef4dc033f..0000000000 --- a/sdk-tests/sdk-compressible-test/src/errors.rs +++ /dev/null @@ -1,92 +0,0 @@ -use anchor_lang::prelude::*; - -#[repr(u32)] -pub enum ErrorCode { - InvalidAccountCount, - InvalidRentRecipient, - MintCreationFailed, - MissingCompressedTokenProgram, - MissingCompressedTokenProgramAuthorityPDA, - RentRecipientMismatch, - InvalidAccountDiscriminator, - DerivedTokenAccountMismatch, - MissingAuthority, - MissingCpiContext, -} - -#[automatically_derived] -impl ::core::fmt::Debug for ErrorCode { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::write_str( - f, - match self { - ErrorCode::InvalidAccountCount => "InvalidAccountCount", - ErrorCode::InvalidRentRecipient => "InvalidRentRecipient", - ErrorCode::MintCreationFailed => "MintCreationFailed", - ErrorCode::MissingCompressedTokenProgram => "MissingCompressedTokenProgram", - ErrorCode::MissingCompressedTokenProgramAuthorityPDA => { - "MissingCompressedTokenProgramAuthorityPDA" - } - ErrorCode::RentRecipientMismatch => "RentRecipientMismatch", - ErrorCode::InvalidAccountDiscriminator => "InvalidAccountDiscriminator", - ErrorCode::DerivedTokenAccountMismatch => "DerivedTokenAccountMismatch", - ErrorCode::MissingAuthority => "MissingAuthority", - ErrorCode::MissingCpiContext => "MissingCpiContext", - }, - ) - } -} - -impl std::fmt::Display for ErrorCode { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - match self { - ErrorCode::InvalidAccountCount => fmt.write_fmt(format_args!( - "Invalid account count: PDAs and compressed accounts must match", - )), - ErrorCode::InvalidRentRecipient => { - fmt.write_fmt(format_args!("Rent recipient does not match config")) - } - ErrorCode::MintCreationFailed => { - fmt.write_fmt(format_args!("Failed to create compressed mint")) - } - ErrorCode::MissingCompressedTokenProgram => fmt.write_fmt(format_args!( - "Compressed token program account not found in remaining accounts", - )), - ErrorCode::MissingCompressedTokenProgramAuthorityPDA => fmt.write_fmt(format_args!( - "Compressed token program authority PDA account not found in remaining accounts", - )), - ErrorCode::RentRecipientMismatch => { - fmt.write_fmt(format_args!("Rent recipient does not match config")) - } - ErrorCode::InvalidAccountDiscriminator => fmt.write_fmt(format_args!( - "Trying to compress account with invalid discriminator" - )), - ErrorCode::DerivedTokenAccountMismatch => fmt.write_fmt(format_args!( - "Derived token account address must match owner_info.key" - )), - ErrorCode::MissingAuthority => fmt.write_fmt(format_args!( - "Authority account is missing from CPI accounts" - )), - ErrorCode::MissingCpiContext => fmt.write_fmt(format_args!( - "CPI context account is missing from CPI accounts" - )), - } - } -} - -impl From for ProgramError { - fn from(e: ErrorCode) -> Self { - ProgramError::Custom(e as u32) - } -} - -#[repr(u32)] -pub enum CompressibleInstructionError { - InvalidRentRecipient, - CTokenDecompressionNotImplemented, - PdaDecompressionNotImplemented, - TokenCompressionNotImplemented, - PdaCompressionNotImplemented, - MissingSeedAccount, -} diff --git a/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs b/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs deleted file mode 100644 index aaa5492cbb..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs +++ /dev/null @@ -1,223 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::compressible::OPTION_COMPRESSION_INFO_SPACE; - -/// CompressAccountsIdempotent, DecompressAccountsIdempotent, -/// InitializeCompressionConfig, UpdateCompressionConfig accounts are all -/// auto-generated by compressible_instructions macro. -use crate::state::*; - -#[derive(Accounts)] -pub struct CreateRecord<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - init, - payer = user, - // discriminator + owner + string len + name + score + - // option. Note that in the onchain space - // CompressionInfo is always Some. - space = 8 + 32 + 4 + 32 + 8 + OPTION_COMPRESSION_INFO_SPACE, - seeds = [b"user_record", user.key().as_ref()], - bump, - )] - pub user_record: Account<'info, UserRecord>, - /// Needs to be here for the init anchor macro to work. - pub system_program: Program<'info, System>, - /// The global config account - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -#[instruction(placeholder_id: u64)] -pub struct CreatePlaceholderRecord<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - init, - payer = user, - // discriminator + compression_info + owner + string len + name + placeholder_id - space = 8 + OPTION_COMPRESSION_INFO_SPACE + 32 + 4 + 32 + 8, - seeds = [b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], - bump, - )] - pub placeholder_record: Account<'info, PlaceholderRecord>, - /// Needs to be here for the init anchor macro to work. - pub system_program: Program<'info, System>, - /// The global config account - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -#[instruction(account_data: AccountCreationData)] -pub struct CreateUserRecordAndGameSession<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - init, - payer = user, - // discriminator + owner + string len + name + score + - // option. Note that in the onchain space - // CompressionInfo is always Some. - space = 8 + 32 + 4 + 32 + 8 + OPTION_COMPRESSION_INFO_SPACE, - seeds = [b"user_record", user.key().as_ref()], - bump, - )] - pub user_record: Account<'info, UserRecord>, - #[account( - init, - payer = user, - // discriminator + option + session_id + player + - // string len + game_type + start_time + end_time(Option) + score - space = 8 + OPTION_COMPRESSION_INFO_SPACE + 8 + 32 + 4 + 32 + 8 + 9 + 8, - seeds = [b"game_session", account_data.session_id.to_le_bytes().as_ref()], - bump, - )] - pub game_session: Account<'info, GameSession>, - - // Compressed mint creation accounts - only token-specific ones needed - /// The mint signer used for PDA derivation - pub mint_signer: Signer<'info>, - - /// The mint authority used for PDA derivation - pub mint_authority: Signer<'info>, - - /// Compressed token program - /// CHECK: Program ID validated using LIGHT_TOKEN_PROGRAM_ID constant - pub ctoken_program: UncheckedAccount<'info>, - - /// CHECK: CPI authority of the compressed token program - pub compress_token_program_cpi_authority: UncheckedAccount<'info>, - - /// Needs to be here for the init anchor macro to work. - pub system_program: Program<'info, System>, - /// The global config account - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -#[instruction(session_id: u64)] -pub struct CreateGameSession<'info> { - #[account(mut)] - pub player: Signer<'info>, - #[account( - init, - payer = player, - space = 8 + 24 + 8 + 32 + 4 + 32 + 8 + 9 + 8, // discriminator + compression_info + session_id + player + string len + game_type + start_time + end_time(Option) + score - seeds = [b"game_session", session_id.to_le_bytes().as_ref()], - bump, - )] - pub game_session: Account<'info, GameSession>, - pub system_program: Program<'info, System>, - /// The global config account - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct UpdateRecord<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - mut, - seeds = [b"user_record", user.key().as_ref()], - bump, - constraint = user_record.owner == user.key() - )] - pub user_record: Account<'info, UserRecord>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -#[instruction(session_id: u64)] -pub struct UpdateGameSession<'info> { - #[account(mut)] - pub player: Signer<'info>, - #[account( - mut, - seeds = [b"game_session", session_id.to_le_bytes().as_ref()], - bump, - constraint = game_session.player == player.key() - )] - pub game_session: Account<'info, GameSession>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct CompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// The global config account - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct DecompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// The global config account - /// CHECK: load_checked. - pub config: AccountInfo<'info>, - /// UNCHECKED: Anyone can pay to init PDAs. - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - /// CHECK: anyone can pay (optional - only needed if decompressing tokens) - #[account(mut)] - pub ctoken_rent_sponsor: Option>, - /// CHECK: checked by SDK - pub ctoken_config: Option>, - /// CHECK: - pub ctoken_program: Option>, - /// CHECK: - pub ctoken_cpi_authority: Option>, - /// CHECK: unchecked. - pub some_mint: UncheckedAccount<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct InitializeCompressionConfig<'info> { - #[account(mut)] - pub payer: Signer<'info>, - /// CHECK: Config PDA is created and validated by the SDK - #[account(mut)] - pub config: AccountInfo<'info>, - /// The program's data account - /// CHECK: Program data account is validated by the SDK - pub program_data: AccountInfo<'info>, - /// The program's upgrade authority (must sign) - pub authority: Signer<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct UpdateCompressionConfig<'info> { - /// CHECK: Config PDA is created and validated by the SDK - #[account(mut)] - pub config: AccountInfo<'info>, - /// Must match the update authority stored in config - pub authority: Signer<'info>, -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs b/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs deleted file mode 100644 index 4e8a41acdb..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs +++ /dev/null @@ -1,135 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::{ - compressible::{compress_account::prepare_account_for_compression, CompressibleConfig}, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - instruction::{account_meta::CompressedAccountMetaNoLamportsNoAddress, ValidityProof}, - LightDiscriminator, -}; - -/// Auto-generated by compressible_instructions macro. -use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; - -pub fn compress_accounts_idempotent<'info>( - ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, - proof: ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, -) -> Result<()> { - let compression_config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - if ctx.accounts.rent_sponsor.key() != compression_config.rent_sponsor { - msg!( - "rent recipient passed: {:?}", - ctx.accounts.rent_sponsor.key() - ); - msg!( - "rent recipient config: {:?}", - compression_config.rent_sponsor - ); - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - let cpi_accounts = CpiAccounts::new( - ctx.accounts.fee_payer.as_ref(), - &ctx.remaining_accounts[system_accounts_offset as usize..], - LIGHT_CPI_SIGNER, - ); - - let system_accounts_end = cpi_accounts.system_accounts_end_offset(); - let solana_accounts = &cpi_accounts.to_account_infos()[system_accounts_end..]; - - let mut compressed_pda_infos = Vec::new(); - let mut pda_indices_to_close: Vec = Vec::new(); - let mut compressed_account_idx = 0; - - for (i, account_info) in solana_accounts.iter().enumerate() { - if account_info.data_is_empty() { - msg!("No data. Account already compressed or uninitialized. Skipping."); - continue; - } - if account_info.owner == &crate::ID { - let data = account_info.try_borrow_data()?; - let discriminator = &data[0..8]; - let meta = compressed_accounts[compressed_account_idx]; - compressed_account_idx += 1; - - // TODO: consider CHECKING seeds. - match discriminator { - d if d == UserRecord::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = UserRecord::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - &crate::ID, - account_info, - &mut account_data, - &meta, - &cpi_accounts, - &compression_config.address_space, - )?; - - compressed_pda_infos.push(compressed_info); - pda_indices_to_close.push(i); - } - d if d == GameSession::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = GameSession::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - &crate::ID, - account_info, - &mut account_data, - &meta, - &cpi_accounts, - &compression_config.address_space, - )?; - - compressed_pda_infos.push(compressed_info); - pda_indices_to_close.push(i); - } - d if d == PlaceholderRecord::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = - PlaceholderRecord::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - &crate::ID, - account_info, - &mut account_data, - &meta, - &cpi_accounts, - &compression_config.address_space, - )?; - - compressed_pda_infos.push(compressed_info); - pda_indices_to_close.push(i); - } - _ => { - return Err(ProgramError::from(ErrorCode::InvalidAccountDiscriminator).into()); - } - } - } - } - let has_pdas = !compressed_pda_infos.is_empty(); - if has_pdas { - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) - .with_account_infos(&compressed_pda_infos) - .invoke(cpi_accounts)?; - - // Close - for idx in pda_indices_to_close.into_iter() { - let mut info = solana_accounts[idx].clone(); - light_sdk::compressible::close::close(&mut info, ctx.accounts.rent_sponsor.clone()) - .map_err(anchor_lang::prelude::ProgramError::from)?; - } - } - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs deleted file mode 100644 index c478821ecd..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs +++ /dev/null @@ -1,77 +0,0 @@ -use anchor_lang::{prelude::*, solana_program::sysvar::clock::Clock}; -use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - instruction::{PackedAddressTreeInfo, ValidityProof}, -}; - -use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; - -pub fn create_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, - session_id: u64, - game_type: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, -) -> Result<()> { - let game_session = &mut ctx.accounts.game_session; - - // Load config from the config account - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - // Set your account data. - game_session.session_id = session_id; - game_session.player = ctx.accounts.player.key(); - game_session.game_type = game_type; - game_session.start_time = Clock::get()?.unix_timestamp as u64; - game_session.end_time = None; - game_session.score = 0; - - // Check that rent recipient matches your config. - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - // Create CPI accounts. - let player_account_info = ctx.accounts.player.to_account_info(); - let cpi_accounts = CpiAccounts::new( - &player_account_info, - ctx.remaining_accounts, - LIGHT_CPI_SIGNER, - ); - - // Prepare new address params. The cpda takes the address of the - // compressible pda account as seed. - let new_address_params = address_tree_info - .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(0)); - - let game_session_info = game_session.to_account_info(); - let game_session_data_mut = &mut **game_session; - let compressed_info = prepare_compressed_account_on_init::( - &game_session_info, - game_session_data_mut, - &config, - compressed_address, - new_address_params, - output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, // with_data - )?; - - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_new_addresses(&[new_address_params]) - .with_account_infos(&[compressed_info]) - .invoke(cpi_accounts)?; - - game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs deleted file mode 100644 index f3ee307cdd..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs +++ /dev/null @@ -1,68 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - instruction::{PackedAddressTreeInfo, ValidityProof}, -}; - -use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; - -pub fn create_placeholder_record<'info>( - ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, - placeholder_id: u64, - name: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, -) -> Result<()> { - let placeholder_record = &mut ctx.accounts.placeholder_record; - - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - placeholder_record.owner = ctx.accounts.user.key(); - placeholder_record.name = name; - placeholder_record.placeholder_id = placeholder_id; - - // Verify rent recipient matches config - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - // Create CPI accounts - let user_account_info = ctx.accounts.user.to_account_info(); - let cpi_accounts = - CpiAccounts::new(&user_account_info, ctx.remaining_accounts, LIGHT_CPI_SIGNER); - - let new_address_params = address_tree_info.into_new_address_params_assigned_packed( - placeholder_record.key().to_bytes().into(), - Some(0), - ); - - let placeholder_info = placeholder_record.to_account_info(); - let placeholder_data_mut = &mut **placeholder_record; - let compressed_info = prepare_compressed_account_on_init::( - &placeholder_info, - placeholder_data_mut, - &config, - compressed_address, - new_address_params, - output_state_tree_index, - &cpi_accounts, - &config.address_space, - false, // with_data = false for empty compressed account - )?; - - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_new_addresses(&[new_address_params]) - .with_account_infos(&[compressed_info]) - .invoke(cpi_accounts)?; - - // Note: PDA is NOT closed in this example (compression_info is set, account remains) - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs deleted file mode 100644 index 81d918c412..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs +++ /dev/null @@ -1,68 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - instruction::{PackedAddressTreeInfo, ValidityProof}, -}; - -use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; - -pub fn create_record<'info>( - ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, - name: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, -) -> Result<()> { - let user_record = &mut ctx.accounts.user_record; - - // 1. Load config from the config account - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - user_record.owner = ctx.accounts.user.key(); - user_record.name = name; - user_record.score = 11; - - // 2. Verify rent recipient matches config - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - // 3. Create CPI accounts - let user_account_info = ctx.accounts.user.to_account_info(); - let cpi_accounts = - CpiAccounts::new(&user_account_info, ctx.remaining_accounts, LIGHT_CPI_SIGNER); - - let new_address_params = address_tree_info - .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); - - let user_record_info = user_record.to_account_info(); - let user_record_data_mut = &mut **user_record; - let compressed_info = prepare_compressed_account_on_init::( - &user_record_info, - user_record_data_mut, - &config, - compressed_address, - new_address_params, - output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, // with_data - )?; - - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_new_addresses(&[new_address_params]) - .with_account_infos(&[compressed_info]) - .invoke(cpi_accounts)?; - - // Close the PDA - user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs deleted file mode 100644 index c5079a9612..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs +++ /dev/null @@ -1,218 +0,0 @@ -use anchor_lang::{ - prelude::*, - solana_program::{instruction::Instruction, program::invoke, sysvar::clock::Clock}, -}; -use light_compressed_account::instruction_data::traits::LightInstructionData; -use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, -}; -use light_sdk_types::{ - cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, -}; -use light_token_interface::instructions::mint_action::{MintToCompressedAction, Recipient}; -use light_token_sdk::compressed_token::{ - create_compressed_mint::find_mint_address, mint_action::MintActionMetaConfig, -}; - -use crate::{errors::ErrorCode, instruction_accounts::*, seeds::*, state::*, LIGHT_CPI_SIGNER}; -pub fn create_user_record_and_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, - account_data: AccountCreationData, - compression_params: CompressionParams, -) -> Result<()> { - let user_record = &mut ctx.accounts.user_record; - let game_session = &mut ctx.accounts.game_session; - - // Load your config checked. - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - // Check that rent recipient matches your config. - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - // Set your account data. - user_record.owner = ctx.accounts.user.key(); - user_record.name = account_data.user_name.clone(); - user_record.score = 11; - - game_session.session_id = account_data.session_id; - game_session.player = ctx.accounts.user.key(); - game_session.game_type = account_data.game_type.clone(); - game_session.start_time = Clock::get()?.unix_timestamp as u64; - game_session.end_time = None; - game_session.score = 0; - - // Create CPI accounts from remaining accounts - let cpi_accounts = CpiAccounts::new_with_config( - ctx.accounts.user.as_ref(), - ctx.remaining_accounts, - CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), - ); - let cpi_context_pubkey = cpi_accounts.cpi_context().unwrap().key(); - let cpi_context_account = cpi_accounts.cpi_context().unwrap(); - - // Prepare new address params. One per pda account. - let user_new_address_params = compression_params - .user_address_tree_info - .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); - let game_new_address_params = compression_params - .game_address_tree_info - .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(1)); - - let mut all_compressed_infos = Vec::new(); - - // Prepare user record for compression - let user_record_info = user_record.to_account_info(); - let user_record_data_mut = &mut **user_record; - let user_compressed_info = prepare_compressed_account_on_init::( - &user_record_info, - user_record_data_mut, - &config, - compression_params.user_compressed_address, - user_new_address_params, - compression_params.user_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, // with_data - )?; - - all_compressed_infos.push(user_compressed_info); - - // Prepare game session for compression - let game_session_info = game_session.to_account_info(); - let game_session_data_mut = &mut **game_session; - let game_compressed_info = prepare_compressed_account_on_init::( - &game_session_info, - game_session_data_mut, - &config, - compression_params.game_compressed_address, - game_new_address_params, - compression_params.game_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, // with_data - )?; - all_compressed_infos.push(game_compressed_info); - - let cpi_context_accounts = CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts.authority().unwrap(), - cpi_context: cpi_context_account, - cpi_signer: LIGHT_CPI_SIGNER, - }; - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, compression_params.proof) - .with_new_addresses(&[user_new_address_params, game_new_address_params]) - .with_account_infos(&all_compressed_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(cpi_context_accounts)?; - - // these are custom seeds of the caller program that are used to derive the program owned onchain tokenb account PDA. - // dual use: as owner of the compressed token account. - let mint = find_mint_address(&ctx.accounts.mint_signer.key()).0; - let (_, token_account_address) = get_ctoken_signer_seeds(&ctx.accounts.user.key(), &mint); - - let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; // Same tree as PDA - let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; // Same tree as PDA - - let proof = compression_params.proof.0.unwrap_or_default(); - let mut instruction_data = - light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( - 0, // root_index - proof, - compression_params.mint_with_context.mint.clone().unwrap(), - ) - .with_mint_to_compressed(MintToCompressedAction::new(vec![ - Recipient::new( - token_account_address, // TRY: THE DECOMPRESS TOKEN ACCOUNT ADDRESS IS THE OWNER OF ITS COMPRESSIBLED VERSION. - 1000, // Mint the full supply to the user - ), - Recipient::new( - get_ctoken_signer2_seeds(&ctx.accounts.user.key()).1, - 1000, - ), - Recipient::new( - get_ctoken_signer3_seeds(&ctx.accounts.user.key()).1, - 1000, - ), - Recipient::new( - get_ctoken_signer4_seeds( - &ctx.accounts.user.key(), - &ctx.accounts.user.key(), - ) - .1, // user as fee_payer - 1000, - ), - Recipient::new( - get_ctoken_signer5_seeds(&ctx.accounts.user.key(), &mint, 42).1, // Fixed index 42 - 1000, - ), - ])); - - instruction_data = instruction_data.with_cpi_context( - light_token_interface::instructions::mint_action::CpiContext { - address_tree_pubkey: address_tree_pubkey.to_bytes(), - set_context: false, - first_set_context: false, - in_tree_index: 1, // address tree - in_queue_index: 0, - out_queue_index: 0, - token_out_queue_index: 0, - assigned_account_index: 2, - read_only_address_trees: [0; 4], - }, - ); - - // Build account meta config - let mut config = MintActionMetaConfig::new_create_mint( - ctx.accounts.user.key(), // fee_payer - ctx.accounts.mint_authority.key(), - ctx.accounts.mint_signer.key(), - address_tree_pubkey, - output_queue, - ) - .with_mint_compressed_tokens(); - - // Set CPI context - config.cpi_context = Some(cpi_context_pubkey); - - // Get account metas - let account_metas = config.to_account_metas(); - - // Serialize instruction data - let data = instruction_data.data().unwrap(); - - // Build instruction - let mint_action_instruction = Instruction { - program_id: Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), - accounts: account_metas, - data, - }; - - // Get all account infos needed for the mint action - let mut account_infos = cpi_accounts.to_account_infos(); - account_infos.push( - ctx.accounts - .compress_token_program_cpi_authority - .to_account_info(), - ); - account_infos.push(ctx.accounts.ctoken_program.to_account_info()); - account_infos.push(ctx.accounts.mint_authority.to_account_info()); - account_infos.push(ctx.accounts.mint_signer.to_account_info()); - account_infos.push(ctx.accounts.user.to_account_info()); - - // Invoke the mint action instruction directly - invoke(&mint_action_instruction, &account_infos)?; - - // at the end of the instruction we always clean up all onchain pdas that we compressed - user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; - game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs b/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs deleted file mode 100644 index 89cc0024ce..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs +++ /dev/null @@ -1,472 +0,0 @@ -// Auto-generated by compressible_instructions macro. -use anchor_lang::prelude::*; -use light_sdk::{ - compressible::{ - decompress_idempotent::{ - into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, - }, - Unpack, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, -}; -use light_sdk_types::cpi_accounts::CpiAccountsConfig; -use light_token_sdk::token::{CompressibleParamsCpi, CreateTokenAccountCpi}; -use solana_program::program_error::ProgramError; - -use crate::{constants::*, errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; -pub fn decompress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, -) -> Result<()> { - // Helper functions to handle each account type - kept out of main frame - #[inline(never)] - #[allow(clippy::too_many_arguments)] - fn handle_user_record<'b, 'info>( - data: UserRecord, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - solana_accounts: &[AccountInfo<'info>], - i: usize, - address_space: Pubkey, - cpi_accounts: &CpiAccounts<'b, 'info>, - rent_sponsor: &AccountInfo<'info>, - out: &mut Vec< - light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, - >, - ) -> Result<()> { - let seeds_vec = { - let seeds: &[&[u8]] = &[USER_RECORD_SEED.as_bytes(), (data.owner).as_ref()]; - let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] - }; - let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); - let infos = prepare_account_for_decompression_idempotent::( - &crate::ID, - data, - into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), - &solana_accounts[i], - rent_sponsor, - cpi_accounts, - seed_refs.as_slice(), - ) - .map_err(ProgramError::from)?; - out.extend(infos); - Ok(()) - } - - #[inline(never)] - #[allow(clippy::too_many_arguments)] - fn handle_game_session<'b, 'info>( - data: GameSession, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - solana_accounts: &[AccountInfo<'info>], - i: usize, - address_space: Pubkey, - cpi_accounts: &CpiAccounts<'b, 'info>, - rent_sponsor: &AccountInfo<'info>, - out: &mut Vec< - light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, - >, - ) -> Result<()> { - let seed_binding_1 = data.session_id.to_le_bytes(); - let seeds_vec = { - let seeds: &[&[u8]] = &["game_session".as_bytes(), seed_binding_1.as_ref()]; - let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] - }; - let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); - let infos = prepare_account_for_decompression_idempotent::( - &crate::ID, - data, - into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), - &solana_accounts[i], - rent_sponsor, - cpi_accounts, - seed_refs.as_slice(), - ) - .map_err(ProgramError::from)?; - out.extend(infos); - Ok(()) - } - - #[inline(never)] - #[allow(clippy::too_many_arguments)] - fn handle_placeholder_record<'b, 'info>( - data: PlaceholderRecord, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - solana_accounts: &[AccountInfo<'info>], - i: usize, - address_space: Pubkey, - cpi_accounts: &CpiAccounts<'b, 'info>, - rent_sponsor: &AccountInfo<'info>, - out: &mut Vec< - light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, - >, - ) -> Result<()> { - let seed_binding_1 = data.placeholder_id.to_le_bytes(); - let seeds_vec = { - let seeds: &[&[u8]] = &["placeholder_record".as_bytes(), seed_binding_1.as_ref()]; - let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] - }; - let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); - let infos = prepare_account_for_decompression_idempotent::( - &crate::ID, - data, - into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), - &solana_accounts[i], - rent_sponsor, - cpi_accounts, - seed_refs.as_slice(), - ) - .map_err(ProgramError::from)?; - out.extend(infos); - Ok(()) - } - - #[inline(never)] - fn check_account_types(compressed_accounts: &[CompressedAccountData]) -> (bool, bool) { - let (mut has_tokens, mut has_pdas) = (false, false); - for c in compressed_accounts { - match c.data { - CompressedAccountVariant::PackedCTokenData(_) => { - has_tokens = true; - } - _ => has_pdas = true, - } - if has_tokens && has_pdas { - break; - } - } - (has_tokens, has_pdas) - } - /// Helper function to process token decompression - separated to avoid stack overflow - #[inline(never)] - #[allow(clippy::too_many_arguments, clippy::extra_unused_lifetimes)] - fn process_tokens<'a, 'b, 'info>( - accounts: &DecompressAccountsIdempotent<'info>, - remaining_accounts: &[anchor_lang::prelude::AccountInfo<'info>], - fee_payer: &anchor_lang::prelude::AccountInfo<'info>, - ctoken_program: &anchor_lang::prelude::UncheckedAccount<'info>, - ctoken_rent_sponsor: &anchor_lang::prelude::AccountInfo<'info>, - ctoken_cpi_authority: &anchor_lang::prelude::UncheckedAccount<'info>, - ctoken_config: &anchor_lang::prelude::AccountInfo<'info>, - config: &anchor_lang::prelude::AccountInfo<'info>, - ctoken_accounts: Vec<( - light_token_sdk::compat::PackedCTokenData, - light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - )>, - proof: light_sdk::instruction::ValidityProof, - cpi_accounts: &CpiAccounts<'b, 'info>, - post_system_accounts: &[anchor_lang::prelude::AccountInfo<'info>], - has_pdas: bool, - ) -> Result<()> { - let mut token_decompress_indices: Box< - Vec, - > = Box::new(Vec::with_capacity(ctoken_accounts.len())); - // Collect per-owner signer seed groups; invoke_signed requires one seed group per PDA signer - let mut token_signers_seed_groups: Vec>> = - Vec::with_capacity(ctoken_accounts.len()); - let packed_accounts = post_system_accounts; - use crate::seeds::ctoken_seed_system::{CTokenSeedContext, CTokenSeedProvider}; - let seed_context = CTokenSeedContext { - accounts, - remaining_accounts, - }; - let authority = cpi_accounts - .authority() - .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingAuthority))?; - let cpi_context = cpi_accounts - .cpi_context() - .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingCpiContext))?; - - for (token_data, meta) in ctoken_accounts.into_iter() { - let owner_index: u8 = token_data.token_data.owner; - let mint_index: u8 = token_data.token_data.mint; - let mint_info = packed_accounts[mint_index as usize].to_account_info(); - let owner_info = packed_accounts[owner_index as usize].to_account_info(); - let (ctoken_signer_seeds, derived_token_account_address) = - token_data.variant.get_seeds(&seed_context); - { - if derived_token_account_address != *owner_info.key { - msg!( - "derived_token_account_address: {:?}", - derived_token_account_address - ); - msg!("owner_info.key: {:?}", owner_info.key); - return Err(ProgramError::from(ErrorCode::DerivedTokenAccountMismatch).into()); - } - - let seed_refs: Vec<&[u8]> = - ctoken_signer_seeds.iter().map(|s| s.as_slice()).collect(); - let seeds_slice: &[&[u8]] = &seed_refs; - - // Build CompressToPubkey from the signer seeds - // The last element is the bump, all preceding elements are the seeds - let bump = ctoken_signer_seeds - .last() - .and_then(|b| b.first().copied()) - .unwrap_or(0); - let seeds_without_bump: Vec> = ctoken_signer_seeds - .iter() - .take(ctoken_signer_seeds.len().saturating_sub(1)) - .cloned() - .collect(); - let compress_to_pubkey = - light_token_interface::instructions::extensions::CompressToPubkey { - bump, - program_id: crate::ID.to_bytes(), - seeds: seeds_without_bump, - }; - - CreateTokenAccountCpi { - payer: fee_payer.clone().to_account_info(), - account: owner_info.clone(), - mint: mint_info.clone(), - owner: *authority.clone().to_account_info().key, - compressible: CompressibleParamsCpi { - compressible_config: ctoken_config.to_account_info(), - rent_sponsor: ctoken_rent_sponsor.clone().to_account_info(), - system_program: accounts.system_program.to_account_info(), - pre_pay_num_epochs: 2, - lamports_per_write: None, - compress_to_account_pubkey: Some(compress_to_pubkey), - token_account_version: - light_token_interface::state::TokenDataVersion::ShaFlat, - compression_only: false, - }, - } - .invoke_signed(&[seeds_slice])?; - } - - // Construct MultiInputTokenDataWithContext from token data and meta - let source = - light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext { - owner: token_data.token_data.owner, - amount: token_data.token_data.amount, - has_delegate: token_data.token_data.has_delegate, - delegate: token_data.token_data.delegate, - mint: token_data.token_data.mint, - version: token_data.token_data.version, - merkle_context: meta.tree_info.into(), - root_index: meta.tree_info.root_index, - }; - let decompress_index = - light_token_sdk::compressed_token::decompress_full::DecompressFullIndices { - source, - destination_index: owner_index, - tlv: None, - }; - token_decompress_indices.push(decompress_index); - token_signers_seed_groups.push(ctoken_signer_seeds); - } - - let ctoken_ix = - light_token_sdk::compressed_token::decompress_full::decompress_full_token_accounts_with_indices( - fee_payer.key(), - proof, - if has_pdas { - Some(cpi_context.key()) - } else { - None - }, - &token_decompress_indices, - packed_accounts, - ) - .map_err(anchor_lang::prelude::ProgramError::from)?; - { - let mut all_account_infos = <[_]>::into_vec(Box::new([fee_payer.to_account_info()])); - all_account_infos.extend(ctoken_cpi_authority.to_account_infos()); - all_account_infos.extend(ctoken_program.to_account_infos()); - all_account_infos.extend(ctoken_rent_sponsor.to_account_infos()); - all_account_infos.extend(config.to_account_infos()); - all_account_infos.extend(cpi_accounts.to_account_infos()); - // Build &[&[&[u8]]] where each inner slice is a distinct PDA seed group - let signer_seed_refs: Vec> = token_signers_seed_groups - .iter() - .map(|group| group.iter().map(|s| s.as_slice()).collect()) - .collect(); - let signer_seed_slices: Vec<&[&[u8]]> = - signer_seed_refs.iter().map(|g| g.as_slice()).collect(); - - anchor_lang::solana_program::program::invoke_signed( - &ctoken_ix, - all_account_infos.as_slice(), - signer_seed_slices.as_slice(), - )?; - } - Ok(()) - } - - let compression_config = light_sdk::compressible::CompressibleConfig::load_checked( - &ctx.accounts.config, - &crate::ID, - )?; - let address_space = compression_config.address_space[0]; - - let (has_tokens, has_pdas) = check_account_types(&compressed_accounts); - if !has_tokens && !has_pdas { - return Ok(()); - } - - // Pre-count for exact alloc. - let (mut token_count, mut pda_count) = (0usize, 0usize); - for c in &compressed_accounts { - match c.data { - CompressedAccountVariant::PackedCTokenData(_) => token_count += 1, - _ => pda_count += 1, - } - } - - let mut ctoken_accounts: Vec<( - light_token_sdk::compat::PackedCTokenData, - light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - )> = Vec::with_capacity(token_count); - let mut compressed_pda_infos = Vec::with_capacity(pda_count); - - let cpi_accounts = if has_tokens { - CpiAccounts::new_with_config( - ctx.accounts.fee_payer.as_ref(), - &ctx.remaining_accounts[system_accounts_offset as usize..], - CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), - ) - } else { - CpiAccounts::new( - ctx.accounts.fee_payer.as_ref(), - &ctx.remaining_accounts[system_accounts_offset as usize..], - LIGHT_CPI_SIGNER, - ) - }; - - let pda_accounts_start = ctx.remaining_accounts.len() - compressed_accounts.len(); - let solana_accounts = &ctx.remaining_accounts[pda_accounts_start..]; - let post_system_offset = cpi_accounts.system_accounts_end_offset(); - let all_infos = cpi_accounts.account_infos(); - let post_system_accounts = &all_infos[post_system_offset..]; - for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { - let unpacked_data = compressed_data.data.unpack(post_system_accounts)?; - match unpacked_data { - CompressedAccountVariant::UserRecord(data) => { - handle_user_record( - data, - &compressed_data.meta, - solana_accounts, - i, - address_space, - &cpi_accounts, - &ctx.accounts.rent_sponsor, - &mut compressed_pda_infos, - )?; - } - CompressedAccountVariant::GameSession(data) => { - handle_game_session( - data, - &compressed_data.meta, - solana_accounts, - i, - address_space, - &cpi_accounts, - &ctx.accounts.rent_sponsor, - &mut compressed_pda_infos, - )?; - } - CompressedAccountVariant::PlaceholderRecord(data) => { - handle_placeholder_record( - data, - &compressed_data.meta, - solana_accounts, - i, - address_space, - &cpi_accounts, - &ctx.accounts.rent_sponsor, - &mut compressed_pda_infos, - )?; - } - CompressedAccountVariant::PackedCTokenData(data) => { - ctoken_accounts.push((data, compressed_data.meta)); - } - CompressedAccountVariant::PackedUserRecord(_) - | CompressedAccountVariant::PackedGameSession(_) - | CompressedAccountVariant::PackedPlaceholderRecord(_) - | CompressedAccountVariant::CTokenData(_) => { - panic!("internal error: entered unreachable code"); - } - } - } - // return if no uninitialized accounts. - let has_pdas = !compressed_pda_infos.is_empty(); - let has_tokens = !ctoken_accounts.is_empty(); - if !has_pdas && !has_tokens { - return Ok(()); - } - let fee_payer = ctx.accounts.fee_payer.as_ref(); - - // init PDAs. - if has_pdas && has_tokens { - let authority = cpi_accounts - .authority() - .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingAuthority))?; - let cpi_context = cpi_accounts - .cpi_context() - .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingCpiContext))?; - let system_cpi_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { - fee_payer, - authority, - cpi_context, - cpi_signer: LIGHT_CPI_SIGNER, - }; - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) - .with_account_infos(&compressed_pda_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(system_cpi_accounts)?; - } else if has_pdas { - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_account_infos(&compressed_pda_infos) - .invoke(cpi_accounts.clone())?; - } - - // init tokens. - if has_tokens { - let ctoken_program = ctx - .accounts - .ctoken_program - .as_ref() - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let ctoken_rent_sponsor = ctx - .accounts - .ctoken_rent_sponsor - .as_ref() - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let ctoken_cpi_authority = ctx - .accounts - .ctoken_cpi_authority - .as_ref() - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let ctoken_config = ctx - .accounts - .ctoken_config - .as_ref() - .ok_or(ProgramError::NotEnoughAccountKeys)?; - - process_tokens( - ctx.accounts, - ctx.remaining_accounts, - fee_payer, - ctoken_program, - ctoken_rent_sponsor, - ctoken_cpi_authority, - ctoken_config, - &ctx.accounts.config, - ctoken_accounts, - proof, - &cpi_accounts, - post_system_accounts, - has_pdas, - )?; - } - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs b/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs deleted file mode 100644 index 9be151ef7c..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Auto-generated by compressible_instructions macro. - -use anchor_lang::prelude::*; -use light_sdk::compressible::process_initialize_compression_config_checked; - -use crate::instruction_accounts::*; - -pub fn initialize_compression_config( - ctx: Context, - rent_sponsor: Pubkey, - address_space: Vec, -) -> Result<()> { - // For tests, set compression_authority to the program's authority (can be a PDA in real apps) - let compression_authority = ctx.accounts.authority.key(); - // Use default rent config for tests - let rent_config = light_compressible::rent::RentConfig::default(); - // Default write_top_up for tests - let write_top_up: u32 = 5_000; - process_initialize_compression_config_checked( - &ctx.accounts.config.to_account_info(), - &ctx.accounts.authority.to_account_info(), - &ctx.accounts.program_data.to_account_info(), - &rent_sponsor, - &compression_authority, - rent_config, - write_top_up, - address_space, - 0, // one global config for now, so bump is 0. - &ctx.accounts.payer.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - &crate::ID, - )?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/mod.rs b/sdk-tests/sdk-compressible-test/src/instructions/mod.rs deleted file mode 100644 index 5c7bd74407..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod compress_accounts_idempotent; -pub mod create_game_session; -pub mod create_placeholder_record; -pub mod create_record; -pub mod create_user_record_and_game_session; -pub mod decompress_accounts_idempotent; -pub mod initialize_compression_config; -pub mod update_compression_config; -pub mod update_game_session; -pub mod update_record; diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs deleted file mode 100644 index 5b505ab161..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Auto-generated by compressible_instructions macro. -use anchor_lang::prelude::*; -use light_sdk::compressible::process_update_compression_config; - -use crate::instruction_accounts::*; - -pub fn update_compression_config( - ctx: Context, - new_rent_sponsor: Option, - new_compression_authority: Option, - new_rent_config: Option, - new_write_top_up: Option, - new_address_space: Option>, - new_update_authority: Option, -) -> Result<()> { - process_update_compression_config( - &ctx.accounts.config.to_account_info(), - &ctx.accounts.authority.to_account_info(), - new_update_authority.as_ref(), - new_rent_sponsor.as_ref(), - new_compression_authority.as_ref(), - new_rent_config, - new_write_top_up, - new_address_space, - &crate::ID, - )?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs deleted file mode 100644 index 0b831ee0a9..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anchor_lang::{prelude::*, solana_program::sysvar::clock::Clock}; -use light_sdk::compressible::HasCompressionInfo; - -use crate::instruction_accounts::*; -pub fn update_game_session( - ctx: Context, - _session_id: u64, - new_score: u64, -) -> Result<()> { - let game_session = &mut ctx.accounts.game_session; - - game_session.score = new_score; - game_session.end_time = Some(Clock::get()?.unix_timestamp as u64); - - // Rent top-up on write using the abstracted method - game_session.compression_info().top_up_rent( - &game_session.to_account_info(), - &ctx.accounts.player.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - )?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs deleted file mode 100644 index 8f76d4282d..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs +++ /dev/null @@ -1,19 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::compressible::HasCompressionInfo; - -use crate::instruction_accounts::*; - -pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { - let user_record = &mut ctx.accounts.user_record; - - user_record.name = name; - user_record.score = score; - - user_record.compression_info().top_up_rent( - &user_record.to_account_info(), - &ctx.accounts.user.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - )?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/lib.rs b/sdk-tests/sdk-compressible-test/src/lib.rs deleted file mode 100644 index cae42204e0..0000000000 --- a/sdk-tests/sdk-compressible-test/src/lib.rs +++ /dev/null @@ -1,178 +0,0 @@ -#![allow(deprecated)] - -use anchor_lang::{prelude::*, solana_program::pubkey::Pubkey}; -use light_sdk::derive_light_cpi_signer; -use light_sdk_types::CpiSigner; - -pub mod constants; -pub mod errors; -pub mod instruction_accounts; -pub mod instructions; -pub mod seeds; -pub mod state; - -pub use constants::*; -pub use errors::*; -pub use instruction_accounts::*; -// Re-export types needed by Anchor's macro expansion -pub use light_sdk::instruction::{ - account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAddressTreeInfo, ValidityProof, -}; -pub use seeds::*; -pub use state::*; - -declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); -pub const LIGHT_CPI_SIGNER: CpiSigner = - derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); - -#[program] -pub mod sdk_compressible_test { - use light_sdk::instruction::{ - account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAddressTreeInfo, - ValidityProof, - }; - - use super::*; - - pub fn create_record<'info>( - ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, - name: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, - ) -> Result<()> { - instructions::create_record::create_record( - ctx, - name, - proof, - compressed_address, - address_tree_info, - output_state_tree_index, - ) - } - - pub fn create_user_record_and_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, - account_data: AccountCreationData, - compression_params: CompressionParams, - ) -> Result<()> { - instructions::create_user_record_and_game_session::create_user_record_and_game_session( - ctx, - account_data, - compression_params, - ) - } - - pub fn update_game_session( - ctx: Context, - _session_id: u64, - new_score: u64, - ) -> Result<()> { - instructions::update_game_session::update_game_session(ctx, _session_id, new_score) - } - - pub fn create_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, - session_id: u64, - game_type: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, - ) -> Result<()> { - instructions::create_game_session::create_game_session( - ctx, - session_id, - game_type, - proof, - compressed_address, - address_tree_info, - output_state_tree_index, - ) - } - - pub fn initialize_compression_config( - ctx: Context, - rent_sponsor: Pubkey, - address_space: Vec, - ) -> Result<()> { - instructions::initialize_compression_config::initialize_compression_config( - ctx, - rent_sponsor, - address_space, - ) - } - - pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { - instructions::update_record::update_record(ctx, name, score) - } - - pub fn create_placeholder_record<'info>( - ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, - placeholder_id: u64, - name: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, - ) -> Result<()> { - instructions::create_placeholder_record::create_placeholder_record( - ctx, - placeholder_id, - name, - proof, - compressed_address, - address_tree_info, - output_state_tree_index, - ) - } - - pub fn decompress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - instructions::decompress_accounts_idempotent::decompress_accounts_idempotent( - ctx, - proof, - compressed_accounts, - system_accounts_offset, - ) - } - - pub fn compress_accounts_idempotent<'info>( - ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, - proof: ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - instructions::compress_accounts_idempotent::compress_accounts_idempotent( - ctx, - proof, - compressed_accounts, - system_accounts_offset, - ) - } - - pub fn update_compression_config( - ctx: Context, - new_rent_sponsor: Option, - new_compression_authority: Option, - new_rent_config: Option, - new_write_top_up: Option, - new_address_space: Option>, - new_update_authority: Option, - ) -> Result<()> { - instructions::update_compression_config::update_compression_config( - ctx, - new_rent_sponsor, - new_compression_authority, - new_rent_config, - new_write_top_up, - new_address_space, - new_update_authority, - ) - } -} diff --git a/sdk-tests/sdk-compressible-test/src/seeds.rs b/sdk-tests/sdk-compressible-test/src/seeds.rs deleted file mode 100644 index 414bbd6d5b..0000000000 --- a/sdk-tests/sdk-compressible-test/src/seeds.rs +++ /dev/null @@ -1,219 +0,0 @@ -// Auto-generated by macro. Seed getter implementations. - -use anchor_lang::prelude::Pubkey; - -use crate::constants::{CTOKEN_SIGNER_SEED, POOL_VAULT_SEED, USER_RECORD_SEED}; - -pub fn get_userrecord_seeds(owner: &Pubkey) -> (Vec>, Pubkey) { - let mut seed_values = Vec::with_capacity(2usize + 1); - seed_values.push((USER_RECORD_SEED.as_bytes()).to_vec()); - seed_values.push((owner.as_ref()).to_vec()); - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} - -pub fn get_gamesession_seeds(session_id: u64) -> (Vec>, Pubkey) { - let mut seed_values = Vec::with_capacity(2usize + 1); - seed_values.push(("game_session".as_bytes()).to_vec()); - seed_values.push((session_id.to_le_bytes().as_ref()).to_vec()); - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} - -pub fn get_placeholderrecord_seeds(placeholder_id: u64) -> (Vec>, Pubkey) { - let mut seed_values = Vec::with_capacity(2usize + 1); - seed_values.push(("placeholder_record".as_bytes()).to_vec()); - seed_values.push((placeholder_id.to_le_bytes().as_ref()).to_vec()); - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} - -pub fn get_ctokensigner_seeds(fee_payer: &Pubkey, some_mint: &Pubkey) -> (Vec>, Pubkey) { - let mut seed_values = Vec::with_capacity(3usize + 1); - seed_values.push((CTOKEN_SIGNER_SEED.as_bytes()).to_vec()); - seed_values.push((fee_payer.as_ref()).to_vec()); - seed_values.push((some_mint.as_ref()).to_vec()); - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} - -pub fn get_ctoken_signer_seeds<'a>(user: &'a Pubkey, mint: &'a Pubkey) -> (Vec>, Pubkey) { - let mut seeds = vec![ - b"ctoken_signer".to_vec(), - user.to_bytes().to_vec(), - mint.to_bytes().to_vec(), - ]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctoken_signer2_seeds(user: &Pubkey) -> (Vec>, Pubkey) { - let mut seeds = vec![b"user_vault".to_vec(), user.to_bytes().to_vec()]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctoken_signer3_seeds(user: &Pubkey) -> (Vec>, Pubkey) { - let mut seeds = vec![ - POOL_VAULT_SEED.as_bytes().to_vec(), - user.to_bytes().to_vec(), - b"liquidity".to_vec(), - ]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctokensigner_authority_seeds() -> (Vec>, Pubkey) { - let mut seeds = vec![b"cpi_authority".to_vec()]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctokensigner2_authority_seeds() -> (Vec>, Pubkey) { - get_ctokensigner_authority_seeds() -} - -pub fn get_ctokensigner3_authority_seeds() -> (Vec>, Pubkey) { - get_ctokensigner_authority_seeds() -} - -pub fn get_ctoken_signer4_seeds<'a>( - user: &'a Pubkey, - fee_payer: &'a Pubkey, -) -> (Vec>, Pubkey) { - let mut seeds = vec![ - b"multi_account".to_vec(), - user.to_bytes().to_vec(), - fee_payer.to_bytes().to_vec(), - crate::ID.to_bytes().to_vec(), - ]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctoken_signer5_seeds<'a>( - user: &'a Pubkey, - mint: &'a Pubkey, - index: u64, -) -> (Vec>, Pubkey) { - let mut seeds = vec![ - b"indexed_vault".to_vec(), - user.to_bytes().to_vec(), - mint.to_bytes().to_vec(), - index.to_le_bytes().to_vec(), - b"final".to_vec(), - ]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctokensigner4_authority_seeds() -> (Vec>, Pubkey) { - get_ctokensigner_authority_seeds() -} - -pub fn get_ctokensigner5_authority_seeds() -> (Vec>, Pubkey) { - get_ctokensigner_authority_seeds() -} - -pub mod ctoken_seed_system { - use anchor_lang::prelude::{AccountInfo, Pubkey}; - - use super::super::{ - constants::{CTOKEN_SIGNER_SEED, POOL_VAULT_SEED}, - instruction_accounts::DecompressAccountsIdempotent, - state::CTokenAccountVariant, - }; - - pub struct CTokenSeedContext<'a, 'info> { - pub accounts: &'a DecompressAccountsIdempotent<'info>, - pub remaining_accounts: &'a [AccountInfo<'info>], - } - - pub trait CTokenSeedProvider { - fn get_seeds<'a, 'info>( - &self, - ctx: &CTokenSeedContext<'a, 'info>, - ) -> (Vec>, Pubkey); - } - - impl CTokenSeedProvider for CTokenAccountVariant { - fn get_seeds<'a, 'info>( - &self, - ctx: &CTokenSeedContext<'a, 'info>, - ) -> (Vec>, Pubkey) { - match self { - CTokenAccountVariant::CTokenSigner => { - let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); - let seed_2 = ctx.accounts.some_mint.key.to_bytes(); - let seeds: &[&[u8]] = &[CTOKEN_SIGNER_SEED.as_bytes(), &seed_1, &seed_2]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); - seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); - (seeds_vec, pda) - } - CTokenAccountVariant::CTokenSigner2 => { - let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); - let seeds: &[&[u8]] = &[b"user_vault", &seed_1]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); - seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); - (seeds_vec, pda) - } - CTokenAccountVariant::CTokenSigner3 => { - let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); - let seeds: &[&[u8]] = &[POOL_VAULT_SEED.as_bytes(), &seed_1, b"liquidity"]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); - seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); - (seeds_vec, pda) - } - CTokenAccountVariant::CTokenSigner4 => { - let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); - let seed_2 = ctx.accounts.fee_payer.key.to_bytes(); - let program_id_bytes = crate::ID.to_bytes(); - let seeds: &[&[u8]] = &[b"multi_account", &seed_1, &seed_2, &program_id_bytes]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); - seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); - (seeds_vec, pda) - } - CTokenAccountVariant::CTokenSigner5 => { - let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); - let seed_2 = ctx.accounts.some_mint.key.to_bytes(); - let index_bytes = 42u64.to_le_bytes(); - let seeds: &[&[u8]] = - &[b"indexed_vault", &seed_1, &seed_2, &index_bytes, b"final"]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); - seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); - (seeds_vec, pda) - } - } - } - } -} diff --git a/sdk-tests/sdk-compressible-test/src/state.rs b/sdk-tests/sdk-compressible-test/src/state.rs deleted file mode 100644 index 3cca9dc377..0000000000 --- a/sdk-tests/sdk-compressible-test/src/state.rs +++ /dev/null @@ -1,522 +0,0 @@ -// Only CompressionParams is custom to the caller program. All other structs are -// auto-generated by macro. Compressed account variant, pack, unpack, -// hasCompressionInfo implementions. - -use anchor_lang::prelude::*; -use light_sdk::{ - account::Size, - compressible::{ - CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack as SdkPack, - Unpack as SdkUnpack, - }, - instruction::{ - account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, - PackedAddressTreeInfo, ValidityProof, - }, - LightDiscriminator, LightHasher, -}; -use light_token_interface::instructions::mint_action::CompressedMintWithContext; -use light_token_sdk::pack::Pack as _TokenPack; - -#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] -#[repr(u8)] -pub enum CTokenAccountVariant { - CTokenSigner = 0, - CTokenSigner2 = 1, - CTokenSigner3 = 2, - CTokenSigner4 = 3, - CTokenSigner5 = 4, -} - -#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] -pub enum CompressedAccountVariant { - UserRecord(UserRecord), - PackedUserRecord(PackedUserRecord), - GameSession(GameSession), - PackedGameSession(PackedGameSession), - PlaceholderRecord(PlaceholderRecord), - PackedPlaceholderRecord(PackedPlaceholderRecord), - PackedCTokenData(light_token_sdk::compat::PackedCTokenData), - CTokenData(light_token_sdk::compat::CTokenData), -} - -impl Default for CompressedAccountVariant { - fn default() -> Self { - Self::UserRecord(UserRecord::default()) - } -} - -impl LightDiscriminator for CompressedAccountVariant { - const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; // This won't be used directly - const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; -} - -impl HasCompressionInfo for CompressedAccountVariant { - fn compression_info(&self) -> &CompressionInfo { - match self { - Self::UserRecord(data) => data.compression_info(), - Self::PackedUserRecord(_) => unreachable!(), - Self::GameSession(data) => data.compression_info(), - Self::PlaceholderRecord(data) => data.compression_info(), - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } - - fn compression_info_mut(&mut self) -> &mut CompressionInfo { - match self { - Self::UserRecord(data) => data.compression_info_mut(), - Self::PackedUserRecord(_) => unreachable!(), - Self::GameSession(data) => data.compression_info_mut(), - Self::PlaceholderRecord(data) => data.compression_info_mut(), - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } - - fn compression_info_mut_opt(&mut self) -> &mut Option { - match self { - Self::UserRecord(data) => data.compression_info_mut_opt(), - Self::PackedUserRecord(_) => unreachable!(), - Self::GameSession(data) => data.compression_info_mut_opt(), - Self::PlaceholderRecord(data) => data.compression_info_mut_opt(), - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } - - fn set_compression_info_none(&mut self) { - match self { - Self::UserRecord(data) => data.set_compression_info_none(), - Self::PackedUserRecord(_) => unreachable!(), - Self::GameSession(data) => data.set_compression_info_none(), - Self::PlaceholderRecord(data) => data.set_compression_info_none(), - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } -} - -impl Size for CompressedAccountVariant { - fn size(&self) -> usize { - match self { - Self::UserRecord(data) => data.size(), - Self::PackedUserRecord(_) => unreachable!(), - Self::GameSession(data) => data.size(), - Self::PlaceholderRecord(data) => data.size(), - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } -} - -// Pack implementation for CompressedAccountVariant -// This delegates to the underlying type's Pack implementation -impl SdkPack for CompressedAccountVariant { - type Packed = Self; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - match self { - Self::PackedUserRecord(_) => unreachable!(), - Self::UserRecord(data) => Self::PackedUserRecord(data.pack(remaining_accounts)), - Self::GameSession(data) => Self::GameSession(data.pack(remaining_accounts)), - Self::PlaceholderRecord(data) => Self::PlaceholderRecord(data.pack(remaining_accounts)), - Self::PackedCTokenData(_) => { - unreachable!() - } - Self::CTokenData(data) => Self::PackedCTokenData(data.pack(remaining_accounts)), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } -} - -// Unpack implementation for CompressedAccountVariant -// This delegates to the underlying type's Unpack implementation -impl SdkUnpack for CompressedAccountVariant { - type Unpacked = Self; - - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - match self { - Self::PackedUserRecord(data) => Ok(Self::UserRecord(data.unpack(remaining_accounts)?)), - Self::UserRecord(_) => unreachable!(), - Self::GameSession(data) => Ok(Self::GameSession(data.unpack(remaining_accounts)?)), - Self::PlaceholderRecord(data) => { - Ok(Self::PlaceholderRecord(data.unpack(remaining_accounts)?)) - } - Self::PackedCTokenData(_data) => Ok(self.clone()), // as-is - Self::CTokenData(_data) => unreachable!(), // as-is - Self::PackedGameSession(_data) => unreachable!(), - Self::PackedPlaceholderRecord(_data) => unreachable!(), - } - } -} - -// Auto-derived via macro. Ix data implemented for Variant. -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct CompressedAccountData { - pub meta: CompressedAccountMetaNoLamportsNoAddress, - pub data: CompressedAccountVariant, -} - -#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] -#[account] -pub struct UserRecord { - #[skip] - pub compression_info: Option, - #[hash] - pub owner: Pubkey, - #[max_len(32)] - pub name: String, - pub score: u64, -} - -// Auto-derived via macro. -impl HasCompressionInfo for UserRecord { - fn compression_info(&self) -> &CompressionInfo { - self.compression_info - .as_ref() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut(&mut self) -> &mut CompressionInfo { - self.compression_info - .as_mut() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut_opt(&mut self) -> &mut Option { - &mut self.compression_info - } - - fn set_compression_info_none(&mut self) { - self.compression_info = None; - } -} - -impl CompressedInitSpace for UserRecord { - const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; -} - -impl CompressedInitSpace for GameSession { - const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; -} - -impl CompressedInitSpace for PlaceholderRecord { - const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; -} - -impl Size for UserRecord { - fn size(&self) -> usize { - Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE - } -} - -impl CompressAs for UserRecord { - type Output = Self; - - fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { - // Simple case: return owned data with compression_info = None - // We can't return Cow::Borrowed because compression_info must always be None for compressed storage - std::borrow::Cow::Owned(Self { - compression_info: None, // ALWAYS None for compressed storage - owner: self.owner, - name: self.name.clone(), - score: self.score, - }) - } -} - -#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] -pub struct PackedUserRecord { - pub compression_info: Option, - pub owner: u8, - pub name: String, - pub score: u64, -} - -// Identity Pack implementation - no custom packing needed for PDA types -impl SdkPack for UserRecord { - type Packed = PackedUserRecord; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - PackedUserRecord { - compression_info: None, - owner: remaining_accounts.insert_or_get(self.owner), - name: self.name.clone(), - score: self.score, - } - } -} - -// Identity Unpack implementation - PDA types are sent unpacked -impl SdkUnpack for UserRecord { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) - } -} - -// Identity Pack implementation - no custom packing needed for PDA types -impl SdkPack for PackedUserRecord { - type Packed = Self; - - fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { - self.clone() - } -} - -// Identity Unpack implementation - PDA types are sent unpacked -impl SdkUnpack for PackedUserRecord { - type Unpacked = UserRecord; - - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(UserRecord { - compression_info: None, - owner: *remaining_accounts[self.owner as usize].key, - name: self.name.clone(), - score: self.score, - }) - } -} - -// Your existing account structs must be manually extended: -// 1. Add compression_info field to the struct, with type -// Option. -// 2. add a #[skip] field for the compression_info field. -// 3. Add LightHasher, LightDiscriminator. -// 4. Add #[hash] attribute to ALL fields that can be >31 bytes. (eg Pubkeys, -// Strings) -#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] -#[account] -pub struct GameSession { - #[skip] - pub compression_info: Option, - pub session_id: u64, - #[hash] - pub player: Pubkey, - #[max_len(32)] - pub game_type: String, - pub start_time: u64, - pub end_time: Option, - pub score: u64, -} - -// Auto-derived via macro. -impl HasCompressionInfo for GameSession { - fn compression_info(&self) -> &CompressionInfo { - self.compression_info - .as_ref() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut(&mut self) -> &mut CompressionInfo { - self.compression_info - .as_mut() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut_opt(&mut self) -> &mut Option { - &mut self.compression_info - } - - fn set_compression_info_none(&mut self) { - self.compression_info = None; - } -} - -impl Size for GameSession { - fn size(&self) -> usize { - Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE - } -} - -impl CompressAs for GameSession { - type Output = Self; - - fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { - // Custom compression: return owned data with modified fields - std::borrow::Cow::Owned(Self { - compression_info: None, // ALWAYS None for compressed storage - session_id: self.session_id, // KEEP - identifier - player: self.player, // KEEP - identifier - game_type: self.game_type.clone(), // KEEP - core property - start_time: 0, // RESET - clear timing - end_time: None, // RESET - clear timing - score: 0, // RESET - clear progress - }) - } -} - -// Identity Pack implementation - no custom packing needed for PDA types -impl SdkPack for GameSession { - type Packed = Self; - - fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { - self.clone() - } -} - -// Identity Unpack implementation - PDA types are sent unpacked -impl SdkUnpack for GameSession { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) - } -} - -// PlaceholderRecord - demonstrates empty compressed account creation -// The PDA remains intact while an empty compressed account is created -#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] -#[account] -pub struct PlaceholderRecord { - #[skip] - pub compression_info: Option, - #[hash] - pub owner: Pubkey, - #[max_len(32)] - pub name: String, - pub placeholder_id: u64, -} - -impl HasCompressionInfo for PlaceholderRecord { - fn compression_info(&self) -> &CompressionInfo { - self.compression_info - .as_ref() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut(&mut self) -> &mut CompressionInfo { - self.compression_info - .as_mut() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut_opt(&mut self) -> &mut Option { - &mut self.compression_info - } - - fn set_compression_info_none(&mut self) { - self.compression_info = None; - } -} - -impl Size for PlaceholderRecord { - fn size(&self) -> usize { - Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE - } -} - -impl CompressAs for PlaceholderRecord { - type Output = Self; - - fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { - std::borrow::Cow::Owned(Self { - compression_info: None, - owner: self.owner, - name: self.name.clone(), - placeholder_id: self.placeholder_id, - }) - } -} - -// Identity Pack implementation - no custom packing needed for PDA types -impl SdkPack for PlaceholderRecord { - type Packed = Self; - - fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { - self.clone() - } -} - -// Identity Unpack implementation - PDA types are sent unpacked -impl SdkUnpack for PlaceholderRecord { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) - } -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct PackedGameSession { - pub compression_info: Option, - pub session_id: u64, - pub player: u8, - pub game_type: String, - pub start_time: u64, - pub end_time: Option, - pub score: u64, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct PackedPlaceholderRecord { - pub compression_info: Option, - pub owner: u8, - pub name: String, - pub placeholder_id: u64, -} - -// Add these struct definitions before the program module -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct AccountCreationData { - pub user_name: String, - pub session_id: u64, - pub game_type: String, - // TODO: Add mint metadata fields when implementing mint functionality - pub mint_name: String, - pub mint_symbol: String, - pub mint_uri: String, - pub mint_decimals: u8, - pub mint_supply: u64, - pub mint_update_authority: Option, - pub mint_freeze_authority: Option, - pub additional_metadata: Option>, -} - -/// Information about a token account to compress -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct TokenAccountInfo { - pub user: Pubkey, - pub mint: Pubkey, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct CompressionParams { - pub proof: ValidityProof, - pub user_compressed_address: [u8; 32], - pub user_address_tree_info: PackedAddressTreeInfo, - pub user_output_state_tree_index: u8, - pub game_compressed_address: [u8; 32], - pub game_address_tree_info: PackedAddressTreeInfo, - pub game_output_state_tree_index: u8, - pub mint_bump: u8, - pub mint_with_context: CompressedMintWithContext, -} diff --git a/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs b/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs deleted file mode 100644 index f1ad826dcd..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs +++ /dev/null @@ -1,228 +0,0 @@ -use anchor_lang::{AccountDeserialize, AnchorDeserialize, Discriminator, ToAccountMetas}; -use light_compressed_account::address::derive_address; -use light_compressible_client::compressible_instruction; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - Indexer, ProgramTestConfig, Rpc, -}; -use light_sdk::compressible::{CompressAs, CompressibleConfig}; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -mod helpers; -use helpers::{create_game_session, ADDRESS_SPACE, RENT_SPONSOR}; - -// Test: create, decompress game session, compress with custom data at -// compression -#[tokio::test] -async fn test_custom_compression_game_session() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let session_id = 42424u64; - let (game_session_pda, _game_bump) = Pubkey::find_program_address( - &[b"game_session", session_id.to_le_bytes().as_ref()], - &program_id, - ); - - create_game_session( - &mut rpc, - &payer, - &program_id, - &config_pda, - &game_session_pda, - session_id, - None, - ) - .await; - - rpc.warp_to_slot(100).unwrap(); - - decompress_single_game_session( - &mut rpc, - &payer, - &program_id, - &game_session_pda, - &_game_bump, - session_id, - "Battle Royale", - 100, - 0, - ) - .await; - - rpc.warp_to_slot(250).unwrap(); - - compress_game_session_with_custom_data( - &mut rpc, - &payer, - &program_id, - &game_session_pda, - session_id, - ) - .await; -} - -#[allow(clippy::too_many_arguments)] -pub async fn decompress_single_game_session( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - game_session_pda: &Pubkey, - _game_bump: &u8, - session_id: u64, - expected_game_type: &str, - expected_slot: u64, - expected_score: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_game_pda = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let game_account_data = c_game_pda.data.as_ref().unwrap(); - let c_game_session = - sdk_compressible_test::GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![c_game_pda.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - program_id, - &compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[*game_session_pda], - &[( - c_game_pda, - sdk_compressible_test::CompressedAccountVariant::GameSession(c_game_session), - )], - &sdk_compressible_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: None, - ctoken_config: None, - ctoken_program: None, - ctoken_cpi_authority: None, - some_mint: payer.pubkey(), - system_program: Pubkey::default(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - assert!(result.is_ok(), "Decompress transaction should succeed"); - - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "Game PDA account data len must be > 0 after decompression" - ); - - let game_pda_data = game_pda_account.unwrap().data; - assert_eq!( - &game_pda_data[0..8], - sdk_compressible_test::GameSession::DISCRIMINATOR, - "Game account anchor discriminator mismatch" - ); - - let decompressed_game_session = - sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); - assert_eq!(decompressed_game_session.session_id, session_id); - assert_eq!(decompressed_game_session.game_type, expected_game_type); - assert_eq!(decompressed_game_session.player, payer.pubkey()); - assert_eq!(decompressed_game_session.score, expected_score); - assert!(!decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); -} - -pub async fn compress_game_session_with_custom_data( - rpc: &mut LightProgramTest, - _payer: &Keypair, - _program_id: &Pubkey, - game_session_pda: &Pubkey, - _session_id: u64, -) { - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap().unwrap(); - let game_pda_data = game_pda_account.data; - let original_game_session = - sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); - - let custom_compressed_data = match original_game_session.compress_as() { - std::borrow::Cow::Borrowed(data) => data.clone(), - std::borrow::Cow::Owned(data) => data, - }; - - assert_eq!( - custom_compressed_data.session_id, original_game_session.session_id, - "Session ID should be kept" - ); - assert_eq!( - custom_compressed_data.player, original_game_session.player, - "Player should be kept" - ); - assert_eq!( - custom_compressed_data.game_type, original_game_session.game_type, - "Game type should be kept" - ); - assert_eq!( - custom_compressed_data.start_time, 0, - "Start time should be RESET to 0" - ); - assert_eq!( - custom_compressed_data.end_time, None, - "End time should be RESET to None" - ); - assert_eq!( - custom_compressed_data.score, 0, - "Score should be RESET to 0" - ); -} diff --git a/sdk-tests/sdk-compressible-test/tests/helpers.rs b/sdk-tests/sdk-compressible-test/tests/helpers.rs deleted file mode 100644 index 38ae537ec5..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/helpers.rs +++ /dev/null @@ -1,334 +0,0 @@ -// Common test helpers and constants for all test files -#![allow(dead_code)] - -use anchor_lang::{ - AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, -}; -use light_compressed_account::address::derive_address; -use light_compressible_client::compressible_instruction; -use light_macros::pubkey; -use light_program_test::{program_test::LightProgramTest, AddressWithTree, Indexer, Rpc}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use sdk_compressible_test::{CompressedAccountVariant, GameSession, UserRecord}; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -pub const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx")]; -pub const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); -pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); - -pub const CTOKEN_RENT_SPONSOR: Pubkey = pubkey!("r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti"); -pub const CTOKEN_RENT_AUTHORITY: Pubkey = pubkey!("8r3QmazwoLHYppYWysXPgUxYJ3Khn7vh3e313jYDcCKy"); - -// Helper functions used across multiple test files - -pub async fn create_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - state_tree_queue: Option, -) { - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new(*program_id); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let accounts = sdk_compressible_test::accounts::CreateRecord { - user: payer.pubkey(), - user_record: *user_record_pda, - system_program: solana_sdk::system_program::ID, - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: RENT_SPONSOR, - }; - - let compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![AddressWithTree { - address: compressed_address, - tree: address_tree_pubkey, - }], - None, - ) - .await - .unwrap() - .value; - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let address_tree_info = packed_tree_infos.address_trees[0]; - - let output_state_tree_index = remaining_accounts.insert_or_get( - state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), - ); - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = sdk_compressible_test::instruction::CreateRecord { - name: "Test User".to_string(), - proof: rpc_result.proof, - compressed_address, - address_tree_info, - output_state_tree_index, - }; - - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!(result.is_ok(), "Transaction should succeed"); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.is_none(), - "Account should not exist after compression" - ); - - let compressed_user_record = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!(compressed_user_record.address, Some(compressed_address)); - assert!(compressed_user_record.data.is_some()); - - let buf = compressed_user_record.data.unwrap().data; - - let user_record = UserRecord::deserialize(&mut &buf[..]).unwrap(); - - assert_eq!(user_record.name, "Test User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, payer.pubkey()); - assert!(user_record.compression_info.is_none()); -} - -pub async fn decompress_single_user_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - _user_record_bump: &u8, - expected_user_name: &str, - expected_slot: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let user_account_data = c_user_pda.data.as_ref().unwrap(); - let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![c_user_pda.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - program_id, - &compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[*user_record_pda], - &[( - c_user_pda, - CompressedAccountVariant::UserRecord(c_user_record), - )], - &sdk_compressible_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: None, - ctoken_config: None, - ctoken_program: None, - ctoken_cpi_authority: None, - some_mint: payer.pubkey(), - system_program: Pubkey::default(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert_eq!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), - 0, - "User PDA account data len must be 0 before decompression" - ); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - assert!(result.is_ok(), "Decompress transaction should succeed"); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - - let compressed_account = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - assert!(compressed_account.data.unwrap().data.is_empty()); - - assert!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "User PDA account data len must be > 0 after decompression" - ); - - let user_pda_data = user_pda_account.unwrap().data; - assert_eq!( - &user_pda_data[0..8], - UserRecord::DISCRIMINATOR, - "User account anchor discriminator mismatch" - ); - - let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); - assert_eq!(decompressed_user_record.name, expected_user_name); - assert_eq!(decompressed_user_record.score, 11); - assert_eq!(decompressed_user_record.owner, payer.pubkey()); - assert!(!decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); -} - -pub async fn create_game_session( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - config_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, - state_tree_queue: Option, -) { - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new(*program_id); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let accounts = sdk_compressible_test::accounts::CreateGameSession { - player: payer.pubkey(), - game_session: *game_session_pda, - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_sponsor: RENT_SPONSOR, - }; - - let compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![AddressWithTree { - address: compressed_address, - tree: address_tree_pubkey, - }], - None, - ) - .await - .unwrap() - .value; - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let address_tree_info = packed_tree_infos.address_trees[0]; - - let output_state_tree_index = remaining_accounts.insert_or_get( - state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), - ); - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = sdk_compressible_test::instruction::CreateGameSession { - session_id, - game_type: "Battle Royale".to_string(), - proof: rpc_result.proof, - compressed_address, - address_tree_info, - output_state_tree_index, - }; - - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!(result.is_ok(), "Transaction should succeed"); - - let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_session_account.is_none(), - "Account should not exist after compression" - ); - - let compressed_game_session = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!(compressed_game_session.address, Some(compressed_address)); - assert!(compressed_game_session.data.is_some()); - - let buf = compressed_game_session.data.as_ref().unwrap().data.clone(); - - let game_session = GameSession::deserialize(&mut &buf[..]).unwrap(); - - assert_eq!(game_session.session_id, session_id); - assert_eq!(game_session.game_type, "Battle Royale"); - assert_eq!(game_session.player, payer.pubkey()); - assert_eq!(game_session.score, 0); - assert!(game_session.compression_info.is_none()); -} diff --git a/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs b/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs deleted file mode 100644 index af75d13450..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs +++ /dev/null @@ -1,137 +0,0 @@ -use anchor_lang::{AccountDeserialize, AnchorDeserialize, ToAccountMetas}; -use light_compressed_account::address::derive_address; -use light_compressible_client::compressible_instruction; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - Indexer, ProgramTestConfig, Rpc, -}; -use light_sdk::compressible::CompressibleConfig; -use sdk_compressible_test::{CompressedAccountVariant, UserRecord}; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -mod helpers; -use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; - -#[tokio::test] -async fn test_double_decompression_attack() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - 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 result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let (user_record_pda, user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - - create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let compressed_user_record = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - let c_user_record = - UserRecord::deserialize(&mut &compressed_user_record.data.unwrap().data[..]).unwrap(); - - rpc.warp_to_slot(100).unwrap(); - - decompress_single_user_record( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &user_record_bump, - "Test User", - 100, - ) - .await; - - let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); - assert!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "User PDA should be decompressed after first operation" - ); - - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![c_user_pda.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - &program_id, - &compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[user_record_pda], - &[( - c_user_pda, - CompressedAccountVariant::UserRecord(c_user_record), - )], - &sdk_compressible_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(&program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: None, - ctoken_config: None, - ctoken_program: None, - ctoken_cpi_authority: None, - some_mint: payer.pubkey(), - system_program: Pubkey::default(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) - .await; - - assert!( - result.is_ok(), - "Second decompression should succeed idempotently" - ); - - let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); - let user_pda_data = user_pda_account.unwrap().data; - let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); - - assert_eq!(decompressed_user_record.name, "Test User"); - assert_eq!(decompressed_user_record.score, 11); - assert_eq!(decompressed_user_record.owner, payer.pubkey()); - assert!(!decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); -} diff --git a/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs b/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs deleted file mode 100644 index 8e037f4533..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs +++ /dev/null @@ -1,1379 +0,0 @@ -use anchor_lang::{ - AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, -}; -use light_client::indexer::CompressedAccount; -use light_compressed_account::address::derive_address; -use light_compressible_client::compressible_instruction; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - AddressWithTree, Indexer, ProgramTestConfig, Rpc, -}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; -use light_token_interface::{ - instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, - state::CompressedMintMetadata, -}; -use light_token_sdk::{ - compressed_token::create_compressed_mint::{derive_mint_compressed_address, find_mint_address}, - pack::compat::CTokenDataWithVariant, - token, -}; -use light_token_types::CPI_AUTHORITY_PDA; -use sdk_compressible_test::{ - get_ctoken_signer2_seeds, get_ctoken_signer3_seeds, get_ctoken_signer4_seeds, - get_ctoken_signer5_seeds, get_ctoken_signer_seeds, CTokenAccountVariant, - CompressedAccountVariant, GameSession, UserRecord, -}; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -mod helpers; -use helpers::{create_game_session, create_record, ADDRESS_SPACE, RENT_SPONSOR}; - -// Tests -// 1. create and decompress two accounts and compress token accounts after -// decompression -// 2. create and decompress accounts with different state trees -#[tokio::test] -async fn test_create_and_decompress_two_accounts() { - let program_id = sdk_compressible_test::ID; - let mut config = - ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_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 config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![crate::helpers::ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let combined_user = Keypair::new(); - let fund_user_ix = solana_sdk::system_instruction::transfer( - &payer.pubkey(), - &combined_user.pubkey(), - 1e9 as u64, - ); - let fund_result = rpc - .create_and_send_transaction(&[fund_user_ix], &payer.pubkey(), &[&payer]) - .await; - assert!(fund_result.is_ok(), "Funding combined user should succeed"); - let combined_session_id = 99999u64; - let (combined_user_record_pda, _combined_user_record_bump) = Pubkey::find_program_address( - &[b"user_record", combined_user.pubkey().as_ref()], - &program_id, - ); - let (combined_game_session_pda, _combined_game_bump) = Pubkey::find_program_address( - &[b"game_session", combined_session_id.to_le_bytes().as_ref()], - &program_id, - ); - - let ( - ctoken_account, - _mint_signer, - ctoken_account_2, - ctoken_account_3, - ctoken_account_4, - ctoken_account_5, - ) = create_user_record_and_game_session( - &mut rpc, - &combined_user, - &program_id, - &config_pda, - &combined_user_record_pda, - &combined_game_session_pda, - combined_session_id, - ) - .await; - - rpc.warp_to_slot(200).unwrap(); - - let (_, ctoken_account_address) = sdk_compressible_test::get_ctoken_signer_seeds( - &combined_user.pubkey(), - &ctoken_account.token.mint, - ); - - let (_, ctoken_account_address_2) = - sdk_compressible_test::get_ctoken_signer2_seeds(&combined_user.pubkey()); - - let (_, ctoken_account_address_3) = - sdk_compressible_test::get_ctoken_signer3_seeds(&combined_user.pubkey()); - - let (_, ctoken_account_address_4) = sdk_compressible_test::get_ctoken_signer4_seeds( - &combined_user.pubkey(), - &combined_user.pubkey(), - ); - - let (_, ctoken_account_address_5) = sdk_compressible_test::get_ctoken_signer5_seeds( - &combined_user.pubkey(), - &ctoken_account.token.mint, - 42, - ); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let compressed_user_record_address = derive_address( - &combined_user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let compressed_game_session_address = derive_address( - &combined_game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let user_record_before_decompression: CompressedAccount = rpc - .get_compressed_account(compressed_user_record_address, None) - .await - .unwrap() - .value - .unwrap(); - let game_session_before_decompression: CompressedAccount = rpc - .get_compressed_account(compressed_game_session_address, None) - .await - .unwrap() - .value - .unwrap(); - - decompress_multiple_pdas_with_ctoken( - &mut rpc, - &combined_user, - &program_id, - &combined_user_record_pda, - &combined_game_session_pda, - combined_session_id, - "Combined User", - "Combined Game", - 200, - ctoken_account.clone(), - ctoken_account_address, - ctoken_account_2.clone(), - ctoken_account_address_2, - ctoken_account_3.clone(), - ctoken_account_address_3, - ctoken_account_4.clone(), - ctoken_account_address_4, - ctoken_account_5.clone(), - ctoken_account_address_5, - ) - .await; - - rpc.warp_epoch_forward(1).await.unwrap(); - - compress_token_account_after_decompress( - &mut rpc, - &combined_user, - &program_id, - &config_pda, - ctoken_account_address, - ctoken_account_address_2, - ctoken_account_address_3, - ctoken_account_address_4, - ctoken_account_address_5, - ctoken_account.token.mint, - ctoken_account.token.amount, - &combined_user_record_pda, - &combined_game_session_pda, - combined_session_id, - user_record_before_decompression.hash, - game_session_before_decompression.hash, - ) - .await; -} - -#[allow(clippy::too_many_arguments)] -pub async fn create_user_record_and_game_session( - rpc: &mut LightProgramTest, - user: &Keypair, - program_id: &Pubkey, - config_pda: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, -) -> ( - light_client::indexer::CompressedTokenAccount, - Pubkey, - light_client::indexer::CompressedTokenAccount, - light_client::indexer::CompressedTokenAccount, - light_client::indexer::CompressedTokenAccount, - light_client::indexer::CompressedTokenAccount, -) { - let state_tree_info = rpc.get_random_state_tree_info().unwrap(); - - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new_with_cpi_context( - *program_id, - state_tree_info.cpi_context.unwrap(), - ); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let decimals = 6u8; - let mint_authority_keypair = Keypair::new(); - let mint_authority = mint_authority_keypair.pubkey(); - let freeze_authority = mint_authority; - let mint_signer = Keypair::new(); - let compressed_mint_address = - derive_mint_compressed_address(&mint_signer.pubkey(), &address_tree_pubkey); - - let (spl_mint, mint_bump) = find_mint_address(&mint_signer.pubkey()); - let accounts = sdk_compressible_test::accounts::CreateUserRecordAndGameSession { - user: user.pubkey(), - user_record: *user_record_pda, - game_session: *game_session_pda, - mint_signer: mint_signer.pubkey(), - ctoken_program: LIGHT_TOKEN_PROGRAM_ID.into(), - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_sponsor: RENT_SPONSOR, - mint_authority, - compress_token_program_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), - }; - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![ - AddressWithTree { - address: user_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: game_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: compressed_mint_address, - tree: address_tree_pubkey, - }, - ], - None, - ) - .await - .unwrap() - .value; - - let user_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let game_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let _mint_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let user_address_tree_info = packed_tree_infos.address_trees[0]; - let game_address_tree_info = packed_tree_infos.address_trees[1]; - let mint_address_tree_info = packed_tree_infos.address_trees[2]; - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = sdk_compressible_test::instruction::CreateUserRecordAndGameSession { - account_data: sdk_compressible_test::AccountCreationData { - user_name: "Combined User".to_string(), - session_id, - game_type: "Combined Game".to_string(), - mint_name: "Test Game Token".to_string(), - mint_symbol: "TGT".to_string(), - mint_uri: "https://example.com/token.json".to_string(), - mint_decimals: 9, - mint_supply: 1_000_000_000, - mint_update_authority: Some(mint_authority), - mint_freeze_authority: Some(freeze_authority), - additional_metadata: None, - }, - compression_params: sdk_compressible_test::CompressionParams { - proof: rpc_result.proof, - user_compressed_address, - user_address_tree_info, - user_output_state_tree_index, - game_compressed_address, - game_address_tree_info, - game_output_state_tree_index, - mint_bump, - mint_with_context: CompressedMintWithContext { - leaf_index: 0, - prove_by_index: false, - root_index: mint_address_tree_info.root_index, - address: compressed_mint_address, - mint: Some(CompressedMintInstructionData { - supply: 0, - decimals, - metadata: CompressedMintMetadata { - version: 3, - mint: spl_mint.into(), - cmint_decompressed: false, - mint_signer: mint_signer.pubkey().to_bytes(), - bump: mint_bump, - }, - mint_authority: Some(mint_authority.into()), - freeze_authority: Some(freeze_authority.into()), - extensions: None, - }), - }, - }, - }; - - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction( - &[instruction], - &user.pubkey(), - &[user, &mint_signer, &mint_authority_keypair], - ) - .await; - - assert!( - result.is_ok(), - "Combined creation transaction should succeed" - ); - - let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_record_account.is_none(), - "User record account should not exist after compression" - ); - - let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_session_account.is_none(), - "Game session account should not exist after compression" - ); - - let compressed_user_record = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_user_record.address, - Some(user_compressed_address) - ); - assert!(compressed_user_record.data.is_some()); - - let user_buf = compressed_user_record.data.unwrap().data; - - let user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); - - assert_eq!(user_record.name, "Combined User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, user.pubkey()); - - let compressed_game_session = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_game_session.address, - Some(game_compressed_address) - ); - assert!(compressed_game_session.data.is_some()); - - let game_buf = compressed_game_session.data.unwrap().data; - let game_session = GameSession::deserialize(&mut &game_buf[..]).unwrap(); - assert_eq!(game_session.session_id, session_id); - assert_eq!(game_session.game_type, "Combined Game"); - assert_eq!(game_session.player, user.pubkey()); - assert_eq!(game_session.score, 0); - - let token_account_address = - get_ctoken_signer_seeds(&user.pubkey(), &find_mint_address(&mint_signer.pubkey()).0).1; - - let mint = find_mint_address(&mint_signer.pubkey()).0; - let token_account_address_2 = get_ctoken_signer2_seeds(&user.pubkey()).1; - let token_account_address_3 = get_ctoken_signer3_seeds(&user.pubkey()).1; - let token_account_address_4 = get_ctoken_signer4_seeds(&user.pubkey(), &user.pubkey()).1; - let token_account_address_5 = get_ctoken_signer5_seeds(&user.pubkey(), &mint, 42).1; - - let ctoken_accounts = rpc - .get_compressed_token_accounts_by_owner(&token_account_address, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_2 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_2, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_3 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_3, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_4 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_4, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_5 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_5, None, None) - .await - .unwrap() - .value; - - assert!( - !ctoken_accounts.items.is_empty(), - "Should have at least one compressed token account" - ); - assert!( - !ctoken_accounts_2.items.is_empty(), - "Should have at least one compressed token account 2" - ); - assert!( - !ctoken_accounts_3.items.is_empty(), - "Should have at least one compressed token account 3" - ); - assert!( - !ctoken_accounts_4.items.is_empty(), - "Should have at least one compressed token account 4" - ); - assert!( - !ctoken_accounts_5.items.is_empty(), - "Should have at least one compressed token account 5" - ); - - let ctoken_account = ctoken_accounts.items[0].clone(); - let ctoken_account_2 = ctoken_accounts_2.items[0].clone(); - let ctoken_account_3 = ctoken_accounts_3.items[0].clone(); - let ctoken_account_4 = ctoken_accounts_4.items[0].clone(); - let ctoken_account_5 = ctoken_accounts_5.items[0].clone(); - - ( - ctoken_account, - mint_signer.pubkey(), - ctoken_account_2, - ctoken_account_3, - ctoken_account_4, - ctoken_account_5, - ) -} - -#[allow(clippy::too_many_arguments)] -pub async fn decompress_multiple_pdas_with_ctoken( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, - expected_user_name: &str, - expected_game_type: &str, - expected_slot: u64, - ctoken_account: light_client::indexer::CompressedTokenAccount, - native_token_account: Pubkey, - ctoken_account_2: light_client::indexer::CompressedTokenAccount, - native_token_account_2: Pubkey, - ctoken_account_3: light_client::indexer::CompressedTokenAccount, - native_token_account_3: Pubkey, - ctoken_account_4: light_client::indexer::CompressedTokenAccount, - native_token_account_4: Pubkey, - ctoken_account_5: light_client::indexer::CompressedTokenAccount, - native_token_account_5: Pubkey, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let user_account_data = c_user_pda.data.as_ref().unwrap(); - let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); - - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_game_pda = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - let game_account_data = c_game_pda.data.as_ref().unwrap(); - let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); - - let rpc_result = rpc - .get_validity_proof( - vec![ - c_user_pda.hash, - c_game_pda.hash, - ctoken_account.clone().account.hash, - ctoken_account_2.clone().account.hash, - ctoken_account_3.clone().account.hash, - ctoken_account_4.clone().account.hash, - ctoken_account_5.clone().account.hash, - ], - vec![], - None, - ) - .await - .unwrap() - .value; - - let ctoken_config = token::config_pda(); - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - program_id, - &compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[ - *user_record_pda, - *game_session_pda, - native_token_account, - native_token_account_2, - native_token_account_3, - native_token_account_4, - native_token_account_5, - ], - &[ - ( - c_user_pda.clone(), - CompressedAccountVariant::UserRecord(c_user_record), - ), - ( - c_game_pda.clone(), - CompressedAccountVariant::GameSession(c_game_session), - ), - ( - { - let acc = ctoken_account.clone().account; - let _token = ctoken_account.clone().token; - acc - }, - CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< - CTokenAccountVariant, - > { - variant: CTokenAccountVariant::CTokenSigner, - token_data: ctoken_account.clone().token, - }), - ), - ( - ctoken_account_2.clone().account, - CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< - CTokenAccountVariant, - > { - variant: CTokenAccountVariant::CTokenSigner2, - token_data: ctoken_account_2.clone().token, - }), - ), - ( - ctoken_account_3.clone().account, - CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< - CTokenAccountVariant, - > { - variant: CTokenAccountVariant::CTokenSigner3, - token_data: ctoken_account_3.clone().token, - }), - ), - ( - ctoken_account_4.clone().account, - CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< - CTokenAccountVariant, - > { - variant: CTokenAccountVariant::CTokenSigner4, - token_data: ctoken_account_4.clone().token, - }), - ), - ( - ctoken_account_5.clone().account, - CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< - CTokenAccountVariant, - > { - variant: CTokenAccountVariant::CTokenSigner5, - token_data: ctoken_account_5.clone().token, - }), - ), - ], - &sdk_compressible_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: Some(token::rent_sponsor_pda()), - ctoken_config: Some(ctoken_config), - ctoken_program: Some(token::id()), - ctoken_cpi_authority: Some(token::cpi_authority()), - some_mint: ctoken_account.token.mint, - system_program: Pubkey::default(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert_eq!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), - 0, - "User PDA account data len must be 0 before decompression" - ); - - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert_eq!( - game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), - 0, - "Game PDA account data len must be 0 before decompression" - ); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - assert!(result.is_ok(), "Decompress transaction should succeed"); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "User PDA account data len must be > 0 after decompression" - ); - - let user_pda_data = user_pda_account.unwrap().data; - assert_eq!( - &user_pda_data[0..8], - UserRecord::DISCRIMINATOR, - "User account anchor discriminator mismatch" - ); - - let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); - assert_eq!(decompressed_user_record.name, expected_user_name); - assert_eq!(decompressed_user_record.score, 11); - assert_eq!(decompressed_user_record.owner, payer.pubkey()); - assert!(!decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "Game PDA account data len must be > 0 after decompression" - ); - - let game_pda_data = game_pda_account.unwrap().data; - assert_eq!( - &game_pda_data[0..8], - sdk_compressible_test::GameSession::DISCRIMINATOR, - "Game account anchor discriminator mismatch" - ); - - let decompressed_game_session = - sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); - assert_eq!(decompressed_game_session.session_id, session_id); - assert_eq!(decompressed_game_session.game_type, expected_game_type); - assert_eq!(decompressed_game_session.player, payer.pubkey()); - assert_eq!(decompressed_game_session.score, 0); - assert!(!decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - let token_account_data = rpc - .get_account(native_token_account) - .await - .unwrap() - .unwrap(); - assert!( - !token_account_data.data.is_empty(), - "Token account should have data" - ); - assert_eq!(token_account_data.owner, LIGHT_TOKEN_PROGRAM_ID.into()); - - let compressed_user_record_data = rpc - .get_compressed_account(c_user_pda.clone().address.unwrap(), None) - .await - .unwrap() - .value - .unwrap(); - let compressed_game_session_data = rpc - .get_compressed_account(c_game_pda.clone().address.unwrap(), None) - .await - .unwrap() - .value - .unwrap(); - for ctoken in [ - &ctoken_account, - &ctoken_account_2, - &ctoken_account_3, - &ctoken_account_4, - &ctoken_account_5, - ] { - let response = rpc - .get_compressed_account_by_hash(ctoken.clone().account.hash, None) - .await - .unwrap(); - assert!( - response.value.is_none(), - "Compressed token account should have value == None after being closed" - ); - } - - assert!( - compressed_user_record_data.data.unwrap().data.is_empty(), - "Compressed user record should be closed/empty after decompression" - ); - assert!( - compressed_game_session_data.data.unwrap().data.is_empty(), - "Compressed game session should be closed/empty after decompression" - ); -} - -#[allow(clippy::too_many_arguments)] -pub async fn decompress_multiple_pdas( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, - expected_user_name: &str, - expected_game_type: &str, - expected_slot: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let user_account_data = c_user_pda.data.as_ref().unwrap(); - - let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); - - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_game_pda = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - let game_account_data = c_game_pda.data.as_ref().unwrap(); - - let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![c_user_pda.hash, c_game_pda.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - program_id, - &compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[*user_record_pda, *game_session_pda], - &[ - ( - c_user_pda, - CompressedAccountVariant::UserRecord(c_user_record), - ), - ( - c_game_pda, - CompressedAccountVariant::GameSession(c_game_session), - ), - ], - &sdk_compressible_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: None, - ctoken_config: None, - ctoken_program: None, - ctoken_cpi_authority: None, - some_mint: payer.pubkey(), - system_program: Pubkey::default(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert_eq!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), - 0, - "User PDA account data len must be 0 before decompression" - ); - - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert_eq!( - game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), - 0, - "Game PDA account data len must be 0 before decompression" - ); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - assert!(result.is_ok(), "Decompress transaction should succeed"); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "User PDA account data len must be > 0 after decompression" - ); - - let user_pda_data = user_pda_account.unwrap().data; - assert_eq!( - &user_pda_data[0..8], - UserRecord::DISCRIMINATOR, - "User account anchor discriminator mismatch" - ); - - let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); - assert_eq!(decompressed_user_record.name, expected_user_name); - assert_eq!(decompressed_user_record.score, 11); - assert_eq!(decompressed_user_record.owner, payer.pubkey()); - assert!(!decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "Game PDA account data len must be > 0 after decompression" - ); - - let game_pda_data = game_pda_account.unwrap().data; - assert_eq!( - &game_pda_data[0..8], - sdk_compressible_test::GameSession::DISCRIMINATOR, - "Game account anchor discriminator mismatch" - ); - - let decompressed_game_session = - sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); - assert_eq!(decompressed_game_session.session_id, session_id); - assert_eq!(decompressed_game_session.game_type, expected_game_type); - assert_eq!(decompressed_game_session.player, payer.pubkey()); - assert_eq!(decompressed_game_session.score, 0); - assert!(!decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - let c_game_pda = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert!(c_game_pda.data.is_some()); - assert_eq!(c_game_pda.data.unwrap().data.len(), 0); -} - -#[allow(clippy::too_many_arguments)] -pub async fn compress_token_account_after_decompress( - rpc: &mut LightProgramTest, - user: &Keypair, - program_id: &Pubkey, - _config_pda: &Pubkey, - token_account_address: Pubkey, - _token_account_address_2: Pubkey, - _token_account_address_3: Pubkey, - _token_account_address_4: Pubkey, - _token_account_address_5: Pubkey, - mint: Pubkey, - amount: u64, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, - user_record_hash_before_decompression: [u8; 32], - game_session_hash_before_decompression: [u8; 32], -) { - let token_account_data = rpc.get_account(token_account_address).await.unwrap(); - assert!( - token_account_data.is_some(), - "Token account should exist before compression" - ); - - let account = token_account_data.unwrap(); - - assert!( - account.lamports > 0, - "Token account should have lamports before compression" - ); - assert!( - !account.data.is_empty(), - "Token account should have data before compression" - ); - - let (_user_record_seeds, user_record_pubkey) = - sdk_compressible_test::get_userrecord_seeds(&user.pubkey()); - let (_game_session_seeds, game_session_pubkey) = - sdk_compressible_test::get_gamesession_seeds(session_id); - let (_, token_account_address) = get_ctoken_signer_seeds(&user.pubkey(), &mint); - - let (_, token_account_address_2) = get_ctoken_signer2_seeds(&user.pubkey()); - let (_, token_account_address_3) = get_ctoken_signer3_seeds(&user.pubkey()); - let (_, token_account_address_4) = get_ctoken_signer4_seeds(&user.pubkey(), &user.pubkey()); - let (_, token_account_address_5) = get_ctoken_signer5_seeds(&user.pubkey(), &mint, 42); - let (_token_signer_seeds, _ctoken_1_authority_pda) = - sdk_compressible_test::get_ctokensigner_authority_seeds(); - - let (_token_signer_seeds_2, _ctoken_2_authority_pda) = - sdk_compressible_test::get_ctokensigner2_authority_seeds(); - - let (_token_signer_seeds_3, _ctoken_3_authority_pda) = - sdk_compressible_test::get_ctokensigner3_authority_seeds(); - - let (_token_signer_seeds_4, _ctoken_4_authority_pda) = - sdk_compressible_test::get_ctokensigner4_authority_seeds(); - - let (_token_signer_seeds_5, _ctoken_5_authority_pda) = - sdk_compressible_test::get_ctokensigner5_authority_seeds(); - - let _cpisigner = Pubkey::new_from_array(sdk_compressible_test::LIGHT_CPI_SIGNER.cpi_signer); - - let user_record_account = rpc.get_account(*user_record_pda).await.unwrap().unwrap(); - let game_session_account = rpc.get_account(*game_session_pda).await.unwrap().unwrap(); - let _token_account = rpc - .get_account(token_account_address) - .await - .unwrap() - .unwrap(); - let _token_account_2 = rpc - .get_account(token_account_address_2) - .await - .unwrap() - .unwrap(); - let _token_account_3 = rpc - .get_account(token_account_address_3) - .await - .unwrap() - .unwrap(); - let _token_account_4 = rpc - .get_account(token_account_address_4) - .await - .unwrap() - .unwrap(); - let _token_account_5 = rpc - .get_account(token_account_address_5) - .await - .unwrap() - .unwrap(); - - assert_eq!(*user_record_pda, user_record_pubkey); - assert_eq!(*game_session_pda, game_session_pubkey); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let compressed_user_record_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let compressed_game_session_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let user_record: CompressedAccount = rpc - .get_compressed_account(compressed_user_record_address, None) - .await - .unwrap() - .value - .unwrap(); - let game_session: CompressedAccount = rpc - .get_compressed_account(compressed_game_session_address, None) - .await - .unwrap() - .value - .unwrap(); - - let user_record_hash = user_record.hash; - let game_session_hash = game_session.hash; - - assert_ne!( - user_record_hash, user_record_hash_before_decompression, - "User record hash NOT_EQUAL before and after compression" - ); - assert_ne!( - game_session_hash, game_session_hash_before_decompression, - "Game session hash NOT_EQUAL before and after compression" - ); - - let proof_with_context = rpc - .get_validity_proof(vec![user_record_hash, game_session_hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::compress_accounts_idempotent( - program_id, - sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[user_record_pubkey, game_session_pubkey], - &[user_record_account, game_session_account], - &sdk_compressible_test::accounts::CompressAccountsIdempotent { - fee_payer: user.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: RENT_SPONSOR, - } - .to_account_metas(None), - proof_with_context, - ) - .unwrap(); - - for _account in instruction.accounts.iter() {} - - let result = rpc - .create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) - .await; - - assert!( - result.is_ok(), - "PDA compression should succeed: {:?}", - result - ); - - rpc.warp_slot_forward(20000).await.unwrap(); - - let token_account_after = rpc.get_account(token_account_address).await.unwrap(); - assert!( - token_account_after.is_none(), - "Token account should not exist after compression" - ); - let token_account_after_2 = rpc.get_account(token_account_address_2).await.unwrap(); - assert!( - token_account_after_2.is_none(), - "Token account 2 should not exist after compression" - ); - let token_account_after_3 = rpc.get_account(token_account_address_3).await.unwrap(); - assert!( - token_account_after_3.is_none(), - "Token account 3 should not exist after compression" - ); - let token_account_after_4 = rpc.get_account(token_account_address_4).await.unwrap(); - assert!( - token_account_after_4.is_none(), - "Token account 4 should not exist after compression" - ); - let token_account_after_5 = rpc.get_account(token_account_address_5).await.unwrap(); - assert!( - token_account_after_5.is_none(), - "Token account 5 should not exist after compression" - ); - - let ctoken_accounts = rpc - .get_compressed_token_accounts_by_owner(&token_account_address, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_2 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_2, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_3 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_3, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_4 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_4, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_5 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_5, None, None) - .await - .unwrap() - .value; - - assert!( - !ctoken_accounts.items.is_empty(), - "Should have at least one compressed token account after compression" - ); - assert!( - !ctoken_accounts_2.items.is_empty(), - "Should have at least one compressed token account 2 after compression" - ); - assert!( - !ctoken_accounts_3.items.is_empty(), - "Should have at least one compressed token account 3 after compression" - ); - assert!( - !ctoken_accounts_4.items.is_empty(), - "Should have at least one compressed token account 4 after compression" - ); - assert!( - !ctoken_accounts_5.items.is_empty(), - "Should have at least one compressed token account 5 after compression" - ); - - let ctoken = &ctoken_accounts.items[0]; - assert_eq!( - ctoken.token.mint, mint, - "Compressed token should have the same mint" - ); - assert_eq!( - ctoken.token.owner, token_account_address, - "Compressed token owner should be the token account address" - ); - assert_eq!( - ctoken.token.amount, amount, - "Compressed token should have the same amount" - ); - let ctoken2 = &ctoken_accounts_2.items[0]; - assert_eq!( - ctoken2.token.mint, mint, - "Compressed token 2 should have the same mint" - ); - assert_eq!( - ctoken2.token.owner, token_account_address_2, - "Compressed token 2 owner should be the token account address" - ); - assert_eq!( - ctoken2.token.amount, amount, - "Compressed token 2 should have the same amount" - ); - let ctoken3 = &ctoken_accounts_3.items[0]; - assert_eq!( - ctoken3.token.mint, mint, - "Compressed token 3 should have the same mint" - ); - assert_eq!( - ctoken3.token.owner, token_account_address_3, - "Compressed token 3 owner should be the token account address" - ); - assert_eq!( - ctoken3.token.amount, amount, - "Compressed token 3 should have the same amount" - ); - let ctoken4 = &ctoken_accounts_4.items[0]; - assert_eq!( - ctoken4.token.mint, mint, - "Compressed token 4 should have the same mint" - ); - assert_eq!( - ctoken4.token.owner, token_account_address_4, - "Compressed token 4 owner should be the token account address" - ); - assert_eq!( - ctoken4.token.amount, amount, - "Compressed token 4 should have the same amount" - ); - let ctoken5 = &ctoken_accounts_5.items[0]; - assert_eq!( - ctoken5.token.mint, mint, - "Compressed token 5 should have the same mint" - ); - assert_eq!( - ctoken5.token.owner, token_account_address_5, - "Compressed token 5 owner should be the token account address" - ); - assert_eq!( - ctoken5.token.amount, amount, - "Compressed token 5 should have the same amount" - ); - let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); - let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); - let token_account = rpc.get_account(token_account_address).await.unwrap(); - let token_account_3 = rpc.get_account(token_account_address_3).await.unwrap(); - let token_account_4 = rpc.get_account(token_account_address_4).await.unwrap(); - let token_account_5 = rpc.get_account(token_account_address_5).await.unwrap(); - - assert!( - user_record_account.is_none(), - "User record account should be None" - ); - assert!( - game_session_account.is_none(), - "Game session account should be None" - ); - assert!(token_account.is_none(), "Token account should be None"); - assert!( - user_record_account - .map(|a| a.data.is_empty()) - .unwrap_or(true), - "User record account should be empty" - ); - assert!( - game_session_account - .map(|a| a.data.is_empty()) - .unwrap_or(true), - "Game session account should be empty" - ); - assert!( - token_account.map(|a| a.data.is_empty()).unwrap_or(true), - "Token account should be empty" - ); - assert!( - token_account_3.map(|a| a.data.is_empty()).unwrap_or(true), - "Token account 3 should be empty" - ); - assert!( - token_account_4.map(|a| a.data.is_empty()).unwrap_or(true), - "Token account 4 should be empty" - ); - assert!( - token_account_5.map(|a| a.data.is_empty()).unwrap_or(true), - "Token account 5 should be empty" - ); -} - -#[tokio::test] -async fn test_create_and_decompress_accounts_with_different_state_trees() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let (user_record_pda, _user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - - let session_id = 54321u64; - let (game_session_pda, _game_bump) = Pubkey::find_program_address( - &[b"game_session", session_id.to_le_bytes().as_ref()], - &program_id, - ); - - let first_state_tree_info = rpc.get_state_tree_infos()[0]; - let second_state_tree_info = rpc.get_state_tree_infos()[1]; - - create_record( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - Some(first_state_tree_info.queue), - ) - .await; - - create_game_session( - &mut rpc, - &payer, - &program_id, - &config_pda, - &game_session_pda, - session_id, - Some(second_state_tree_info.queue), - ) - .await; - - rpc.warp_to_slot(100).unwrap(); - - decompress_multiple_pdas( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &game_session_pda, - session_id, - "Test User", - "Battle Royale", - 100, - ) - .await; -} diff --git a/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs b/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs deleted file mode 100644 index cb8adc9d87..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs +++ /dev/null @@ -1,527 +0,0 @@ -use anchor_lang::{AccountDeserialize, Discriminator, InstructionData, ToAccountMetas}; -use light_compressed_account::address::derive_address; -use light_compressible_client::compressible_instruction; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - Indexer, ProgramTestConfig, Rpc, RpcError, -}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use solana_account::Account; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -mod helpers; -use helpers::{ADDRESS_SPACE, RENT_SPONSOR}; - -// Tests for the simplest possible compression flows: -// 1. Create empty compressed account (do not compress at init) -// 2. Idempotent double compression -#[tokio::test] -async fn test_create_empty_compressed_account() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let placeholder_id = 54321u64; - let (placeholder_record_pda, placeholder_record_bump) = Pubkey::find_program_address( - &[b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], - &program_id, - ); - - create_placeholder_record( - &mut rpc, - &payer, - &program_id, - &config_pda, - &placeholder_record_pda, - placeholder_id, - "Test Placeholder", - ) - .await; - - let placeholder_pda_account = rpc.get_account(placeholder_record_pda).await.unwrap(); - assert!( - placeholder_pda_account.is_some(), - "Placeholder PDA should exist after empty compression" - ); - let account = placeholder_pda_account.unwrap(); - assert!( - account.lamports > 0, - "Placeholder PDA should have lamports (not closed)" - ); - assert!( - !account.data.is_empty(), - "Placeholder PDA should have data (not closed)" - ); - - let placeholder_data = account.data; - let decompressed_placeholder_record = - sdk_compressible_test::PlaceholderRecord::try_deserialize(&mut &placeholder_data[..]) - .unwrap(); - assert_eq!(decompressed_placeholder_record.name, "Test Placeholder"); - assert_eq!( - decompressed_placeholder_record.placeholder_id, - placeholder_id - ); - assert_eq!(decompressed_placeholder_record.owner, payer.pubkey()); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - let compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_placeholder = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_placeholder.address, - Some(compressed_address), - "Compressed account should exist with correct address" - ); - assert!( - compressed_placeholder.data.is_some(), - "Compressed account should have data field" - ); - - let compressed_data = compressed_placeholder.data.unwrap(); - assert_eq!( - compressed_data.data.len(), - 0, - "Compressed account data should be empty" - ); - - rpc.warp_to_slot(200).unwrap(); - - compress_placeholder_record( - &mut rpc, - &payer, - &program_id, - &config_pda, - &placeholder_record_pda, - &placeholder_record_bump, - placeholder_id, - ) - .await; -} - -#[tokio::test] -async fn test_double_compression_attack() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let placeholder_id = 99999u64; - let (placeholder_record_pda, _placeholder_record_bump) = Pubkey::find_program_address( - &[b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], - &program_id, - ); - - create_placeholder_record( - &mut rpc, - &payer, - &program_id, - &config_pda, - &placeholder_record_pda, - placeholder_id, - "Double Compression Test", - ) - .await; - - let placeholder_pda_account = rpc.get_account(placeholder_record_pda).await.unwrap(); - assert!( - placeholder_pda_account.is_some(), - "Placeholder PDA should exist before compression" - ); - let account_before = placeholder_pda_account.unwrap(); - assert!( - account_before.lamports > 0, - "Placeholder PDA should have lamports before compression" - ); - assert!( - !account_before.data.is_empty(), - "Placeholder PDA should have data before compression" - ); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - let compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_placeholder_before = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_placeholder_before.address, - Some(compressed_address), - "Empty compressed account should exist" - ); - assert_eq!( - compressed_placeholder_before - .data - .as_ref() - .unwrap() - .data - .len(), - 0, - "Compressed account should be empty initially" - ); - - rpc.warp_to_slot(200).unwrap(); - - let first_compression_result = compress_placeholder_record_for_double_test( - &mut rpc, - &payer, - &program_id, - &placeholder_record_pda, - placeholder_id, - Some(account_before.clone()), - ) - .await; - assert!( - first_compression_result.is_ok(), - "First compression should succeed: {:?}", - first_compression_result - ); - - let placeholder_pda_after_first = rpc.get_account(placeholder_record_pda).await.unwrap(); - assert!( - placeholder_pda_after_first.is_none(), - "PDA should not exist after first compression" - ); - - let compressed_placeholder_after_first = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let first_data_len = compressed_placeholder_after_first - .data - .as_ref() - .unwrap() - .data - .len(); - assert!( - first_data_len > 0, - "Compressed account should contain data after first compression" - ); - - let second_compression_result = compress_placeholder_record_for_double_test( - &mut rpc, - &payer, - &program_id, - &placeholder_record_pda, - placeholder_id, - Some(account_before), - ) - .await; - - assert!( - second_compression_result.is_ok(), - "Second compression should succeed idempotently: {:?}", - second_compression_result - ); - - let placeholder_pda_after_second = rpc.get_account(placeholder_record_pda).await.unwrap(); - assert!( - placeholder_pda_after_second.is_none(), - "PDA should still not exist after second compression" - ); - - let compressed_placeholder_after_second = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_placeholder_after_first.hash, compressed_placeholder_after_second.hash, - "Compressed account hash should be unchanged after second compression" - ); - assert_eq!( - compressed_placeholder_after_first - .data - .as_ref() - .unwrap() - .data, - compressed_placeholder_after_second - .data - .as_ref() - .unwrap() - .data, - "Compressed account data should be unchanged after second compression" - ); -} - -pub async fn create_placeholder_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - config_pda: &Pubkey, - placeholder_record_pda: &Pubkey, - placeholder_id: u64, - name: &str, -) { - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new(*program_id); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let accounts = sdk_compressible_test::accounts::CreatePlaceholderRecord { - user: payer.pubkey(), - placeholder_record: *placeholder_record_pda, - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_sponsor: RENT_SPONSOR, - }; - - let compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![light_program_test::AddressWithTree { - address: compressed_address, - tree: address_tree_pubkey, - }], - None, - ) - .await - .unwrap() - .value; - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let address_tree_info = packed_tree_infos.address_trees[0]; - - let output_state_tree_index = - remaining_accounts.insert_or_get(rpc.get_random_state_tree_info().unwrap().queue); - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = sdk_compressible_test::instruction::CreatePlaceholderRecord { - placeholder_id, - name: name.to_string(), - proof: rpc_result.proof, - compressed_address, - address_tree_info, - output_state_tree_index, - }; - - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!( - result.is_ok(), - "CreatePlaceholderRecord transaction should succeed" - ); -} - -pub async fn compress_placeholder_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - _config_pda: &Pubkey, - placeholder_record_pda: &Pubkey, - _placeholder_record_bump: &u8, - placeholder_id: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let placeholder_compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_placeholder = rpc - .get_compressed_account(placeholder_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) - .await - .unwrap() - .value; - - let _placeholder_seeds = sdk_compressible_test::get_placeholderrecord_seeds(placeholder_id); - - let account = rpc - .get_account(*placeholder_record_pda) - .await - .unwrap() - .unwrap(); - - let instruction = - light_compressible_client::compressible_instruction::compress_accounts_idempotent( - program_id, - sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[*placeholder_record_pda], - &[account], - &sdk_compressible_test::accounts::CompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: RENT_SPONSOR, - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!( - result.is_ok(), - "CompressPlaceholderRecord transaction should succeed: {:?}", - result - ); - - let _account = rpc.get_account(*placeholder_record_pda).await.unwrap(); - - let compressed_placeholder_after = rpc - .get_compressed_account(placeholder_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert!( - compressed_placeholder_after.data.is_some(), - "Compressed account should have data after compression" - ); - - let compressed_data_after = compressed_placeholder_after.data.unwrap(); - - assert!( - !compressed_data_after.data.is_empty(), - "Compressed account should contain the PDA data" - ); -} - -pub async fn compress_placeholder_record_for_double_test( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - placeholder_record_pda: &Pubkey, - placeholder_id: u64, - previous_account: Option, -) -> Result { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let placeholder_compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_placeholder = rpc - .get_compressed_account(placeholder_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) - .await - .unwrap() - .value; - - let _placeholder_seeds = sdk_compressible_test::get_placeholderrecord_seeds(placeholder_id); - - let accounts_to_compress = if let Some(account) = previous_account { - vec![account] - } else { - panic!("Previous account should be provided"); - }; - let instruction = - light_compressible_client::compressible_instruction::compress_accounts_idempotent( - program_id, - sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[*placeholder_record_pda], - &accounts_to_compress, - &sdk_compressible_test::accounts::CompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: RENT_SPONSOR, - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await -} diff --git a/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs b/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs deleted file mode 100644 index 3dab2f194c..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs +++ /dev/null @@ -1,293 +0,0 @@ -use anchor_lang::{ - AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, -}; -use light_compressed_account::address::derive_address; -use light_compressible::rent::{RentConfig, SLOTS_PER_EPOCH}; -use light_compressible_client::compressible_instruction; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - Indexer, ProgramTestConfig, Rpc, RpcError, -}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use sdk_compressible_test::UserRecord; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; -use solana_system_interface::instruction as system_instruction; - -mod helpers; -use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; - -// Tests -// 1. init compressed, decompress, and compress -// 2. update_record bumps compression info -#[tokio::test] -async fn test_create_decompress_compress_single_account() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - 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 result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let (user_record_pda, user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - - create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; - - rpc.warp_to_slot(100).unwrap(); - - decompress_single_user_record( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &user_record_bump, - "Test User", - 100, - ) - .await; - - // Top up PDA so it's initially NOT compressible (sufficiently funded) - // Fund exactly one epoch of rent plus compression_cost, so after one epoch passes it becomes compressible. - let pda_account = rpc.get_account(user_record_pda).await.unwrap().unwrap(); - let bytes = pda_account.data.len() as u64; - let rent_cfg = RentConfig::default(); - let rent_per_epoch = rent_cfg.rent_curve_per_epoch(bytes); - let compression_cost = rent_cfg.compression_cost as u64; - let top_up = rent_per_epoch + compression_cost; - - let transfer_ix = system_instruction::transfer(&payer.pubkey(), &user_record_pda, top_up); - let res = rpc - .create_and_send_transaction(&[transfer_ix], &payer.pubkey(), &[&payer]) - .await; - assert!(res.is_ok(), "Top-up transfer should succeed"); - - // Immediately try to compress – should FAIL because not compressible yet (sufficiently funded) - let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, true).await; - assert!( - result.is_err(), - "Compression should fail while sufficiently funded" - ); - - // Advance one full epoch so required_epochs increases and the account becomes compressible - rpc.warp_to_slot(SLOTS_PER_EPOCH * 2).unwrap(); - - // Now compression should SUCCEED (account no longer sufficiently funded for current+next epoch) - let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, false).await; - assert!( - result.is_ok(), - "Compression should succeed after epochs advance" - ); -} - -#[tokio::test] -async fn test_update_record_compression_info() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - 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 result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let (user_record_pda, user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - - create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; - - rpc.warp_to_slot(100).unwrap(); - decompress_single_user_record( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &user_record_bump, - "Test User", - 100, - ) - .await; - - rpc.warp_to_slot(150).unwrap(); - - let accounts = sdk_compressible_test::accounts::UpdateRecord { - user: payer.pubkey(), - user_record: user_record_pda, - system_program: solana_sdk::system_program::id(), - }; - - let instruction_data = sdk_compressible_test::instruction::UpdateRecord { - name: "Updated User".to_string(), - score: 42, - }; - - let instruction = Instruction { - program_id, - accounts: accounts.to_account_metas(None), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) - .await; - assert!(result.is_ok(), "Update record transaction should succeed"); - - rpc.warp_to_slot(200).unwrap(); - - let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); - assert!( - user_pda_account.is_some(), - "User record account should exist after update" - ); - - let account_data = user_pda_account.unwrap().data; - let updated_user_record = UserRecord::try_deserialize(&mut &account_data[..]).unwrap(); - - assert_eq!(updated_user_record.name, "Updated User"); - assert_eq!(updated_user_record.score, 42); - assert_eq!(updated_user_record.owner, payer.pubkey()); - - assert_eq!( - updated_user_record - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - 100 - ); - assert!(!updated_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); -} - -pub async fn compress_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - should_fail: bool, -) -> Result { - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.is_some(), - "User PDA account should exist before compression" - ); - let account = user_pda_account.unwrap(); - assert!( - account.lamports > 0, - "Account should have lamports before compression" - ); - assert!( - !account.data.is_empty(), - "Account data should not be empty before compression" - ); - - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new(*program_id); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_account = rpc - .get_compressed_account(address, None) - .await - .unwrap() - .value - .unwrap(); - let compressed_address = compressed_account.address.unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![compressed_account.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = compressible_instruction::compress_accounts_idempotent( - program_id, - sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[*user_record_pda], - &[account], - &sdk_compressible_test::accounts::CompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: RENT_SPONSOR, - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - if should_fail { - assert!(result.is_err(), "Compress transaction should fail"); - return result; - } else { - assert!(result.is_ok(), "Compress transaction should succeed"); - } - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.is_none(), - "Account should not exist after compression" - ); - - let compressed_user_record = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!(compressed_user_record.address, Some(compressed_address)); - assert!(compressed_user_record.data.is_some()); - - let buf = compressed_user_record.data.unwrap().data; - let user_record: UserRecord = UserRecord::deserialize(&mut &buf[..]).unwrap(); - - assert_eq!(user_record.name, "Test User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, payer.pubkey()); - assert!(user_record.compression_info.is_none()); - Ok(result.unwrap()) -} diff --git a/sdk-tests/sdk-light-token-test/src/create_ata.rs b/sdk-tests/sdk-light-token-test/src/create_ata.rs index 541a6c2767..ab3f0ca660 100644 --- a/sdk-tests/sdk-light-token-test/src/create_ata.rs +++ b/sdk-tests/sdk-light-token-test/src/create_ata.rs @@ -1,5 +1,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; +<<<<<<< HEAD:sdk-tests/sdk-light-token-test/src/create_ata.rs use light_token_sdk::token::{CompressibleParamsCpi, CreateAssociatedAccountCpi}; +======= +use light_ctoken_sdk::ctoken::CreateCTokenAtaCpi; +>>>>>>> a606eb113 (wip):sdk-tests/sdk-ctoken-test/src/create_ata.rs use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; use crate::{ATA_SEED, ID}; @@ -30,6 +34,7 @@ pub fn process_create_ata_invoke( return Err(ProgramError::NotEnoughAccountKeys); } +<<<<<<< HEAD:sdk-tests/sdk-light-token-test/src/create_ata.rs // Build the compressible params using constructor let compressible_params = CompressibleParamsCpi::new_ata( accounts[5].clone(), @@ -39,15 +44,20 @@ pub fn process_create_ata_invoke( // Use the CreateAssociatedCTokenAccountCpi - owner and mint are AccountInfos CreateAssociatedAccountCpi { +======= + CreateCTokenAtaCpi { + payer: accounts[2].clone(), +>>>>>>> a606eb113 (wip):sdk-tests/sdk-ctoken-test/src/create_ata.rs owner: accounts[0].clone(), mint: accounts[1].clone(), - payer: accounts[2].clone(), - associated_token_account: accounts[3].clone(), - system_program: accounts[4].clone(), + ata: accounts[3].clone(), bump: data.bump, - compressible: compressible_params, - idempotent: false, } + .rent_free( + accounts[5].clone(), // compressible_config + accounts[6].clone(), // rent_sponsor + accounts[4].clone(), // system_program + ) .invoke()?; Ok(()) @@ -79,28 +89,26 @@ pub fn process_create_ata_invoke_signed( return Err(ProgramError::InvalidSeeds); } - // Build the compressible params using constructor - let compressible_params = CompressibleParamsCpi::new_ata( - accounts[5].clone(), - accounts[6].clone(), - accounts[4].clone(), - ); + let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; +<<<<<<< HEAD:sdk-tests/sdk-light-token-test/src/create_ata.rs // Use the CreateAssociatedAccountCpi - owner and mint are AccountInfos let account_infos = CreateAssociatedAccountCpi { +======= + CreateCTokenAtaCpi { + payer: accounts[2].clone(), +>>>>>>> a606eb113 (wip):sdk-tests/sdk-ctoken-test/src/create_ata.rs owner: accounts[0].clone(), mint: accounts[1].clone(), - payer: accounts[2].clone(), - associated_token_account: accounts[3].clone(), - system_program: accounts[4].clone(), + ata: accounts[3].clone(), bump: data.bump, - compressible: compressible_params, - idempotent: false, - }; - - // Invoke with PDA signing - let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; - account_infos.invoke_signed(&[signer_seeds])?; + } + .rent_free( + accounts[5].clone(), // compressible_config + accounts[6].clone(), // rent_sponsor + accounts[4].clone(), // system_program + ) + .invoke_signed(&[signer_seeds])?; Ok(()) } diff --git a/sdk-tests/sdk-light-token-test/src/create_token_account.rs b/sdk-tests/sdk-light-token-test/src/create_token_account.rs index 0325d6aa2f..44badc6632 100644 --- a/sdk-tests/sdk-light-token-test/src/create_token_account.rs +++ b/sdk-tests/sdk-light-token-test/src/create_token_account.rs @@ -40,15 +40,19 @@ pub fn process_create_token_account_invoke( accounts[4].clone(), ); +<<<<<<< HEAD:sdk-tests/sdk-light-token-test/src/create_token_account.rs // Build the account infos struct CreateTokenAccountCpi { +======= + // Build the account infos struct and invoke with custom compressible params + CreateCTokenAccountCpi { +>>>>>>> a606eb113 (wip):sdk-tests/sdk-ctoken-test/src/create_token_account.rs payer: accounts[0].clone(), account: accounts[1].clone(), mint: accounts[2].clone(), owner: data.owner, - compressible: compressible_params, } - .invoke()?; + .invoke_with(compressible_params)?; Ok(()) } @@ -85,18 +89,20 @@ pub fn process_create_token_account_invoke_signed( accounts[4].clone(), ); +<<<<<<< HEAD:sdk-tests/sdk-light-token-test/src/create_token_account.rs // Build the account infos struct let account_infos = CreateTokenAccountCpi { +======= + // Invoke with PDA signing and custom compressible params + let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; + CreateCTokenAccountCpi { +>>>>>>> a606eb113 (wip):sdk-tests/sdk-ctoken-test/src/create_token_account.rs payer: accounts[0].clone(), account: accounts[1].clone(), mint: accounts[2].clone(), owner: data.owner, - compressible: compressible_params, - }; - - // Invoke with PDA signing - let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; - account_infos.invoke_signed(&[signer_seeds])?; + } + .invoke_signed_with(compressible_params, &[signer_seeds])?; Ok(()) } From fa0058db8a58872933f0500a3c57a562eb2aa3b8 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 16 Jan 2026 16:57:45 +0000 Subject: [PATCH 2/9] resolve mc --- Cargo.toml | 6 - .../src/compressible/decompress_context.rs | 10 -- .../macros/src/compressible/seed_providers.rs | 9 - sdk-libs/program-test/src/compressible.rs | 33 ---- .../src/compressible/decompress_runtime.rs | 13 -- sdk-libs/token-sdk/src/token/create.rs | 73 -------- sdk-libs/token-sdk/src/token/create_ata.rs | 61 ------- sdk-libs/token-sdk/src/token/mod.rs | 13 -- sdk-libs/token-sdk/tests/pack_test.rs | 4 +- .../src/instruction_accounts.rs | 36 ---- .../csdk-anchor-full-derived-test/src/lib.rs | 167 ------------------ .../tests/basic_test.rs | 134 -------------- .../sdk-light-token-test/src/create_ata.rs | 21 --- .../src/create_token_account.rs | 10 -- 14 files changed, 2 insertions(+), 588 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 56895bcef3..13dedafe14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,13 +55,7 @@ members = [ "sdk-tests/sdk-native-test", "sdk-tests/sdk-v1-native-test", "sdk-tests/sdk-token-test", -<<<<<<< HEAD - "sdk-tests/sdk-compressible-test", - "sdk-tests/sdk-light-token-test", - "sdk-tests/csdk-anchor-derived-test", -======= "sdk-tests/sdk-ctoken-test", ->>>>>>> a606eb113 (wip) "sdk-tests/csdk-anchor-full-derived-test", "forester-utils", "forester", diff --git a/sdk-libs/macros/src/compressible/decompress_context.rs b/sdk-libs/macros/src/compressible/decompress_context.rs index 12cc0c440e..1a42dfa147 100644 --- a/sdk-libs/macros/src/compressible/decompress_context.rs +++ b/sdk-libs/macros/src/compressible/decompress_context.rs @@ -109,13 +109,8 @@ pub fn generate_decompress_context_trait_impl( Ok(quote! { impl<#lifetime> light_sdk::compressible::DecompressContext<#lifetime> for DecompressAccountsIdempotent<#lifetime> { -<<<<<<< HEAD - type CompressedData = CompressedAccountData; - type PackedTokenData = light_token_sdk::compat::PackedCTokenData<#token_variant_ident>; -======= type CompressedData = RentFreeAccountData; type PackedTokenData = light_ctoken_sdk::compat::PackedCTokenData<#packed_token_variant_ident>; ->>>>>>> a606eb113 (wip) type CompressedMeta = light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; type SeedParams = (); @@ -200,12 +195,7 @@ pub fn generate_decompress_context_trait_impl( post_system_accounts: &[solana_account_info::AccountInfo<#lifetime>], has_prior_context: bool, ) -> std::result::Result<(), solana_program_error::ProgramError> { -<<<<<<< HEAD - light_token_sdk::compressible::process_decompress_tokens_runtime( - self, -======= light_ctoken_sdk::compressible::process_decompress_tokens_runtime( ->>>>>>> a606eb113 (wip) remaining_accounts, fee_payer, token_program, diff --git a/sdk-libs/macros/src/compressible/seed_providers.rs b/sdk-libs/macros/src/compressible/seed_providers.rs index 22ad520b27..ed7cbf4973 100644 --- a/sdk-libs/macros/src/compressible/seed_providers.rs +++ b/sdk-libs/macros/src/compressible/seed_providers.rs @@ -6,10 +6,6 @@ use syn::{spanned::Spanned, Ident, Result}; use crate::compressible::instructions::{InstructionDataSpec, SeedElement, TokenSeedSpec}; -<<<<<<< HEAD -pub fn generate_token_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Result { - let variants = token_seeds.iter().enumerate().map(|(index, spec)| { -======= /// Extract ctx.* field names from seed elements (both token seeds and authority seeds) fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { let mut ctx_fields = Vec::new(); @@ -92,7 +88,6 @@ pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Re // Unpacked variants (with Pubkeys) let unpacked_variants = token_seeds.iter().map(|spec| { ->>>>>>> a606eb113 (wip) let variant_name = &spec.variant; let ctx_fields = extract_ctx_fields_from_token_spec(spec); @@ -225,12 +220,8 @@ pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Re }) } -<<<<<<< HEAD -pub fn generate_token_seed_provider_implementation( -======= /// Phase 8: Generate CTokenSeedProvider impl that uses self.field instead of ctx.accounts.field pub fn generate_ctoken_seed_provider_implementation( ->>>>>>> a606eb113 (wip) token_seeds: &[TokenSeedSpec], ) -> Result { let mut get_seeds_match_arms = Vec::new(); diff --git a/sdk-libs/program-test/src/compressible.rs b/sdk-libs/program-test/src/compressible.rs index 9100e8bcd1..67efeec166 100644 --- a/sdk-libs/program-test/src/compressible.rs +++ b/sdk-libs/program-test/src/compressible.rs @@ -42,38 +42,16 @@ fn determine_account_type(data: &[u8]) -> Option { } } -<<<<<<< HEAD -/// Extracts CompressionInfo and account type from account data, handling both Token and CMint. -/// Returns (CompressionInfo, account_type) or None if parsing fails. -#[cfg(feature = "devenv")] -fn extract_compression_info(data: &[u8]) -> Option<(CompressionInfo, u8)> { - use light_token_interface::state::extensions::ExtensionStruct; -======= /// Extracts CompressionInfo, account type, and compression_only from account data. /// Returns (CompressionInfo, account_type, compression_only) or None if parsing fails. #[cfg(feature = "devenv")] fn extract_compression_info(data: &[u8]) -> Option<(CompressionInfo, u8, bool)> { use light_zero_copy::traits::ZeroCopyAt; ->>>>>>> a606eb113 (wip) let account_type = determine_account_type(data)?; match account_type { ACCOUNT_TYPE_TOKEN_ACCOUNT => { -<<<<<<< HEAD - let ctoken = Token::deserialize(&mut &data[..]).ok()?; - // Get CompressionInfo from Compressible extension - let compression_info = - ctoken - .extensions - .as_ref()? - .iter() - .find_map(|ext| match ext { - ExtensionStruct::Compressible(comp) => Some(comp.info), - _ => None, - })?; - Some((compression_info, account_type)) -======= let (ctoken, _) = CToken::zero_copy_at(data).ok()?; let ext = ctoken.get_compressible_extension()?; @@ -97,7 +75,6 @@ fn extract_compression_info(data: &[u8]) -> Option<(CompressionInfo, u8, bool)> }; let compression_only = ext.compression_only != 0; Some((compression_info, account_type, compression_only)) ->>>>>>> a606eb113 (wip) } ACCOUNT_TYPE_MINT => { let cmint = CompressedMint::deserialize(&mut &data[..]).ok()?; @@ -183,15 +160,10 @@ pub async fn claim_and_compress( .iter() .filter(|e| e.1.data.len() >= 165 && e.1.lamports > 0) { -<<<<<<< HEAD - // Extract compression info and account type, handling both Token and CMint - let Some((compression, account_type)) = extract_compression_info(&account.1.data) else { -======= // Extract compression info, account type, and compression_only let Some((compression, account_type, compression_only)) = extract_compression_info(&account.1.data) else { ->>>>>>> a606eb113 (wip) continue; }; @@ -250,11 +222,6 @@ pub async fn claim_and_compress( match state.calculate_claimable_rent(&compression.rent_config, rent_exemption) { None => { // Account is compressible (has rent deficit) -<<<<<<< HEAD - // Only Token accounts can be compressed via compress_and_close_forester - // CMint accounts have a different compression flow -======= ->>>>>>> a606eb113 (wip) if stored_account.account_type == ACCOUNT_TYPE_TOKEN_ACCOUNT { // CToken accounts - separate by compression_only if stored_account.compression_only { diff --git a/sdk-libs/sdk/src/compressible/decompress_runtime.rs b/sdk-libs/sdk/src/compressible/decompress_runtime.rs index 1a19ba9480..8fe36226fc 100644 --- a/sdk-libs/sdk/src/compressible/decompress_runtime.rs +++ b/sdk-libs/sdk/src/compressible/decompress_runtime.rs @@ -26,17 +26,9 @@ pub trait HasTokenVariant { /// Trait for token seed providers. /// -<<<<<<< HEAD -/// Also defined in compressed-token-sdk for token-specific runtime helpers. -pub trait TokenSeedProvider: Copy { - /// Type of accounts struct needed for seed derivation. - type Accounts<'info>; - -======= /// After Phase 8 refactor: The variant itself contains resolved seed pubkeys, /// so no accounts struct is needed for seed derivation. pub trait CTokenSeedProvider: Copy { ->>>>>>> a606eb113 (wip) /// Get seeds for the token account PDA (used for decompression). fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; @@ -308,10 +300,6 @@ where // Process tokens (if any) - executes and consumes CPI context if PDAs wrote to it if has_tokens { -<<<<<<< HEAD - let token_program = ctx - .token_program() -======= let post_system_offset = cpi_accounts.system_accounts_end_offset(); let all_infos = cpi_accounts.account_infos(); let post_system_accounts = all_infos @@ -320,7 +308,6 @@ where let ctoken_program = ctx .ctoken_program() ->>>>>>> a606eb113 (wip) .ok_or(ProgramError::NotEnoughAccountKeys)?; let token_rent_sponsor = ctx .token_rent_sponsor() diff --git a/sdk-libs/token-sdk/src/token/create.rs b/sdk-libs/token-sdk/src/token/create.rs index b6425e82cf..14dcbbbe12 100644 --- a/sdk-libs/token-sdk/src/token/create.rs +++ b/sdk-libs/token-sdk/src/token/create.rs @@ -1,13 +1,7 @@ use borsh::BorshSerialize; -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create.rs -use light_token_interface::instructions::{ - create_token_account::CreateTokenAccountInstructionData, - extensions::CompressibleExtensionInstructionData, -======= use light_ctoken_interface::instructions::{ create_ctoken_account::CreateTokenAccountInstructionData, extensions::{CompressToPubkey, CompressibleExtensionInstructionData}, ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create.rs }; use solana_account_info::AccountInfo; use solana_cpi::{invoke, invoke_signed}; @@ -90,24 +84,6 @@ impl CreateTokenAccount { } } -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create.rs -/// # Create a ctoken account via CPI: -/// ```rust,no_run -/// # use light_token_sdk::token::{CreateTokenAccountCpi, CompressibleParamsCpi}; -/// # use solana_account_info::AccountInfo; -/// # use solana_pubkey::Pubkey; -/// # let payer: AccountInfo = todo!(); -/// # let account: AccountInfo = todo!(); -/// # let mint: AccountInfo = todo!(); -/// # let owner: Pubkey = todo!(); -/// # let compressible: CompressibleParamsCpi = todo!(); -/// CreateTokenAccountCpi { -/// payer, -/// account, -/// mint, -/// owner, -/// compressible, -======= /// CPI builder for creating CToken accounts (vaults). /// /// # Example - Rent-free vault with PDA signing @@ -117,7 +93,6 @@ impl CreateTokenAccount { /// account: ctx.accounts.vault.to_account_info(), /// mint: ctx.accounts.mint.to_account_info(), /// owner: ctx.accounts.vault_authority.key(), ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create.rs /// } /// .rent_free( /// ctx.accounts.ctoken_config.to_account_info(), @@ -134,27 +109,6 @@ pub struct CreateTokenAccountCpi<'info> { pub owner: Pubkey, } -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create.rs -impl<'info> CreateTokenAccountCpi<'info> { - pub fn new( - payer: AccountInfo<'info>, - account: AccountInfo<'info>, - mint: AccountInfo<'info>, - owner: Pubkey, - compressible: CompressibleParamsCpi<'info>, - ) -> Self { - Self { - payer, - account, - mint, - owner, - compressible, - } - } - - pub fn instruction(&self) -> Result { - CreateTokenAccount::from(self).instruction() -======= impl<'info> CreateCTokenAccountCpi<'info> { /// Enable rent-free mode with compressible config. /// @@ -190,7 +144,6 @@ impl<'info> CreateCTokenAccountCpi<'info> { compressible, } .invoke() ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create.rs } /// Invoke with signing, without rent-free (requires manually constructed compressible params). @@ -339,29 +292,3 @@ impl<'info> LegacyCreateCTokenAccountCpi<'info> { invoke_signed(&instruction, &account_infos, signer_seeds) } } -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create.rs - -impl<'info> From<&CreateTokenAccountCpi<'info>> for CreateTokenAccount { - fn from(account_infos: &CreateTokenAccountCpi<'info>) -> Self { - Self { - payer: *account_infos.payer.key, - account: *account_infos.account.key, - mint: *account_infos.mint.key, - owner: account_infos.owner, - compressible: CompressibleParams { - compressible_config: *account_infos.compressible.compressible_config.key, - rent_sponsor: *account_infos.compressible.rent_sponsor.key, - pre_pay_num_epochs: account_infos.compressible.pre_pay_num_epochs, - lamports_per_write: account_infos.compressible.lamports_per_write, - compress_to_account_pubkey: account_infos - .compressible - .compress_to_account_pubkey - .clone(), - token_account_version: account_infos.compressible.token_account_version, - compression_only: account_infos.compressible.compression_only, - }, - } - } -} -======= ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create.rs diff --git a/sdk-libs/token-sdk/src/token/create_ata.rs b/sdk-libs/token-sdk/src/token/create_ata.rs index 08b3f467e9..4a2330f29f 100644 --- a/sdk-libs/token-sdk/src/token/create_ata.rs +++ b/sdk-libs/token-sdk/src/token/create_ata.rs @@ -132,28 +132,6 @@ impl CreateAssociatedTokenAccount { } } -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create_ata.rs -/// # Create an associated ctoken account via CPI: -/// ```rust,no_run -/// # use light_token_sdk::token::{CreateAssociatedAccountCpi, CompressibleParamsCpi}; -/// # use solana_account_info::AccountInfo; -/// # let owner: AccountInfo = todo!(); -/// # let mint: AccountInfo = todo!(); -/// # let payer: AccountInfo = todo!(); -/// # let associated_token_account: AccountInfo = todo!(); -/// # let system_program: AccountInfo = todo!(); -/// # let bump: u8 = todo!(); -/// # let compressible: CompressibleParamsCpi = todo!(); -/// CreateAssociatedAccountCpi { -/// owner, -/// mint, -/// payer, -/// associated_token_account, -/// system_program, -/// bump, -/// compressible, -/// idempotent: true, -======= /// CPI builder for creating CToken ATAs. /// /// # Example - Rent-free ATA (idempotent) @@ -164,7 +142,6 @@ impl CreateAssociatedTokenAccount { /// mint: ctx.accounts.mint.to_account_info(), /// ata: ctx.accounts.user_ata.to_account_info(), /// bump: params.user_ata_bump, ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs /// } /// .idempotent() /// .rent_free( @@ -174,28 +151,18 @@ impl CreateAssociatedTokenAccount { /// ) /// .invoke()?; /// ``` -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create_ata.rs -pub struct CreateAssociatedAccountCpi<'info> { -======= pub struct CreateCTokenAtaCpi<'info> { pub payer: AccountInfo<'info>, ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs pub owner: AccountInfo<'info>, pub mint: AccountInfo<'info>, pub ata: AccountInfo<'info>, pub bump: u8, } -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create_ata.rs -impl<'info> CreateAssociatedAccountCpi<'info> { - pub fn instruction(&self) -> Result { - CreateAssociatedTokenAccount::from(self).instruction() -======= impl<'info> CreateCTokenAtaCpi<'info> { /// Make this an idempotent create (won't fail if ATA already exists). pub fn idempotent(self) -> CreateCTokenAtaCpiIdempotent<'info> { CreateCTokenAtaCpiIdempotent { base: self } ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs } /// Enable rent-free mode with compressible config. @@ -399,31 +366,3 @@ impl<'info> InternalCreateAtaCpi<'info> { invoke_signed(&instruction, &account_infos, signer_seeds) } } -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/create_ata.rs - -impl<'info> From<&CreateAssociatedAccountCpi<'info>> for CreateAssociatedTokenAccount { - fn from(account_infos: &CreateAssociatedAccountCpi<'info>) -> Self { - Self { - payer: *account_infos.payer.key, - owner: *account_infos.owner.key, - mint: *account_infos.mint.key, - associated_token_account: *account_infos.associated_token_account.key, - bump: account_infos.bump, - compressible: CompressibleParams { - compressible_config: *account_infos.compressible.compressible_config.key, - rent_sponsor: *account_infos.compressible.rent_sponsor.key, - pre_pay_num_epochs: account_infos.compressible.pre_pay_num_epochs, - lamports_per_write: account_infos.compressible.lamports_per_write, - compress_to_account_pubkey: account_infos - .compressible - .compress_to_account_pubkey - .clone(), - token_account_version: account_infos.compressible.token_account_version, - compression_only: account_infos.compressible.compression_only, - }, - idempotent: account_infos.idempotent, - } - } -} -======= ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/create_ata.rs diff --git a/sdk-libs/token-sdk/src/token/mod.rs b/sdk-libs/token-sdk/src/token/mod.rs index c792f57459..96add7a137 100644 --- a/sdk-libs/token-sdk/src/token/mod.rs +++ b/sdk-libs/token-sdk/src/token/mod.rs @@ -3,17 +3,10 @@ //! //! ## Account Creation //! -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/mod.rs -//! - [`CreateAssociatedTokenAccount`] - Create associated ctoken account (ATA) instruction -//! - [`CreateAssociatedTokenAccountCpi`] - Create associated ctoken account (ATA) via CPI -//! - [`CreateTokenAccount`] - Create ctoken account instruction -//! - [`CreateTokenAccountCpi`] - Create ctoken account via CPI -======= //! - [`CreateAssociatedCTokenAccount`] - Create associated ctoken account (ATA) instruction //! - [`CreateCTokenAtaCpi`] - Create associated ctoken account (ATA) via CPI //! - [`CreateCTokenAccount`] - Create ctoken account instruction //! - [`CreateCTokenAccountCpi`] - Create ctoken account via CPI ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/mod.rs //! //! ## Transfers //! @@ -59,16 +52,10 @@ //! # Example: Create rent-free ATA via CPI //! //! ```rust,ignore -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/mod.rs -//! use light_token_sdk::token::{CreateAssociatedTokenAccountCpi, CompressibleParamsCpi}; -//! -//! CreateAssociatedTokenAccountCpi { -======= //! use light_ctoken_sdk::ctoken::CreateCTokenAtaCpi; //! //! CreateCTokenAtaCpi { //! payer: ctx.accounts.payer.to_account_info(), ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/mod.rs //! owner: ctx.accounts.owner.to_account_info(), //! mint: ctx.accounts.mint.to_account_info(), //! ata: ctx.accounts.user_ata.to_account_info(), diff --git a/sdk-libs/token-sdk/tests/pack_test.rs b/sdk-libs/token-sdk/tests/pack_test.rs index 4a42dd2eba..8c80a2889e 100644 --- a/sdk-libs/token-sdk/tests/pack_test.rs +++ b/sdk-libs/token-sdk/tests/pack_test.rs @@ -2,7 +2,7 @@ use light_sdk::instruction::PackedAccounts; use light_token_sdk::{ - compat::{PackedCTokenDataWithVariant, TokenData, TokenDataWithVariant}, + compat::{PackedTokenDataWithVariant, TokenData, TokenDataWithVariant}, pack::Pack, }; use solana_pubkey::Pubkey; @@ -67,7 +67,7 @@ fn test_token_data_with_variant_packing() { }; // Pack the wrapper - let packed: PackedCTokenDataWithVariant = + let packed: PackedTokenDataWithVariant = token_with_variant.pack(&mut remaining_accounts).unwrap(); // Verify variant is unchanged 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 08be08c4b6..d7d72b0aa2 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 @@ -57,15 +57,8 @@ pub struct CreatePdasAndMintAuto<'info> { #[account( init, -<<<<<<< HEAD - payer = user, - // Space: discriminator(8) + session_id(8) + player(32) + game_type_len(4) + - // game_type(32) + start_time(8) + end_time(1+8) + score(8) = 109 bytes - space = 8 + 8 + 32 + 4 + 32 + 8 + 9 + 8, -======= payer = fee_payer, space = 8 + GameSession::INIT_SPACE, ->>>>>>> a606eb113 (wip) seeds = [ b"game_session", crate::max_key(&fee_payer.key(), &authority.key()).as_ref(), @@ -76,36 +69,7 @@ pub struct CreatePdasAndMintAuto<'info> { #[rentfree] pub game_session: Account<'info, GameSession>, -<<<<<<< HEAD - /// Authority signer used in PDA seeds - pub authority: Signer<'info>, - - /// Mint authority signer used in PDA seeds - pub mint_authority: Signer<'info>, - - /// Some account used in PlaceholderRecord PDA seeds - /// CHECK: Used as seed component - pub some_account: AccountInfo<'info>, - - /// Compressed token program - /// CHECK: Program ID validated using LIGHT_TOKEN_PROGRAM_ID constant - pub ctoken_program: UncheckedAccount<'info>, - - /// CHECK: CPI authority of the compressed token program - pub compress_token_program_cpi_authority: UncheckedAccount<'info>, - - /// Needs to be here for the init anchor macro to work. - pub system_program: Program<'info, System>, - - /// Global compressible config - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config -======= /// CHECK: Initialized by mint_action ->>>>>>> a606eb113 (wip) #[account(mut)] #[light_mint( mint_signer = mint_signer, 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 8dd1e1b545..bac069bfb9 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -34,55 +34,12 @@ pub fn program_rent_sponsor() -> Pubkey { Pubkey::from(PROGRAM_RENT_SPONSOR_DATA.0) } -<<<<<<< HEAD -#[add_compressible_instructions( - // Complex PDA account types with seed specifications using BOTH ctx.accounts.* AND data.* - // UserRecord: uses ctx accounts (authority, mint_authority) + data fields (owner, category_id) - UserRecord = ("user_record", ctx.authority, ctx.mint_authority, data.owner, data.category_id.to_le_bytes()), - // GameSession: uses max_key expression with ctx.accounts + data.session_id - GameSession = ("game_session", max_key(&ctx.user.key(), &ctx.authority.key()), data.session_id.to_le_bytes()), - // PlaceholderRecord: mixes ctx accounts and data for seeds - PlaceholderRecord = ("placeholder_record", ctx.authority, ctx.some_account, data.placeholder_id.to_le_bytes(), data.counter.to_le_bytes()), - // Token variant (Light Token account) with authority for compression signing - CTokenSigner = (is_token, "ctoken_signer", ctx.fee_payer, ctx.mint, authority = LIGHT_CPI_SIGNER), - // Instruction data fields used in seed expressions above - owner = Pubkey, - category_id = u64, - session_id = u64, - placeholder_id = u64, - counter = u32, -)] -#[program] -pub mod csdk_anchor_full_derived_test { - #![allow(clippy::too_many_arguments)] - use anchor_lang::solana_program::{program::invoke, sysvar::clock::Clock}; - use light_compressed_account::instruction_data::traits::LightInstructionData; - use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - }; - use light_sdk_types::{ - cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, - }; - use light_token_interface::instructions::mint_action::{ - MintActionCompressedInstructionData, MintToCompressedAction, Recipient, - }; - use light_token_sdk::compressed_token::{ - create_compressed_mint::find_mint_address, mint_action::MintActionMetaConfig, - }; -======= pub const GAME_SESSION_SEED: &str = "game_session"; #[rentfree_program] #[program] pub mod csdk_anchor_full_derived_test { #![allow(clippy::too_many_arguments)] ->>>>>>> a606eb113 (wip) use super::*; use crate::{ @@ -115,99 +72,6 @@ pub mod csdk_anchor_full_derived_test { game_session.end_time = None; game_session.score = 0; -<<<<<<< HEAD - let cpi_accounts = CpiAccounts::new_with_config( - ctx.accounts.user.as_ref(), - ctx.remaining_accounts, - CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), - ); - let cpi_context_pubkey = cpi_accounts.cpi_context().unwrap().key(); - let cpi_context_account = cpi_accounts.cpi_context().unwrap(); - - let user_new_address_params = compression_params - .user_address_tree_info - .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); - let game_new_address_params = compression_params - .game_address_tree_info - .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(1)); - - let mut all_compressed_infos = Vec::new(); - - let user_record_info = user_record.to_account_info(); - let user_record_data_mut = &mut **user_record; - let user_compressed_info = prepare_compressed_account_on_init::( - &user_record_info, - user_record_data_mut, - &config, - compression_params.user_compressed_address, - user_new_address_params, - compression_params.user_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, - )?; - all_compressed_infos.push(user_compressed_info); - - let game_session_info = game_session.to_account_info(); - let game_session_data_mut = &mut **game_session; - let game_compressed_info = prepare_compressed_account_on_init::( - &game_session_info, - game_session_data_mut, - &config, - compression_params.game_compressed_address, - game_new_address_params, - compression_params.game_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, - )?; - all_compressed_infos.push(game_compressed_info); - - let cpi_context_accounts = CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts.authority().unwrap(), - cpi_context: cpi_context_account, - cpi_signer: LIGHT_CPI_SIGNER, - }; - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, compression_params.proof) - .with_new_addresses(&[user_new_address_params, game_new_address_params]) - .with_account_infos(&all_compressed_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(cpi_context_accounts)?; - - let mint = find_mint_address(&ctx.accounts.mint_signer.key()).0; - - // Use the generated client seed function for Light Token signer (generated by add_compressible_instructions macro) - let (_, token_account_address) = get_ctokensigner_seeds(&ctx.accounts.user.key(), &mint); - - let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; - let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; - - // Build instruction data using the correct API - let proof = compression_params.proof.0.unwrap_or_default(); - let instruction_data = MintActionCompressedInstructionData::new_mint( - 0, // root_index for new addresses - proof, - compression_params.mint_with_context.mint.clone().unwrap(), - ) - .with_mint_to_compressed(MintToCompressedAction { - token_account_version: 3, - recipients: vec![Recipient::new(token_account_address, 1000)], - }) - .with_cpi_context( - light_token_interface::instructions::mint_action::CpiContext { - address_tree_pubkey: address_tree_pubkey.to_bytes(), - set_context: false, - first_set_context: false, - in_tree_index: 1, - in_queue_index: 0, - out_queue_index: 0, - token_out_queue_index: 0, - assigned_account_index: 2, - read_only_address_trees: [0; 4], - }, - ); -======= let cmint_key = ctx.accounts.cmint.key(); CreateCTokenAccountCpi { payer: ctx.accounts.fee_payer.to_account_info(), @@ -226,7 +90,6 @@ pub mod csdk_anchor_full_derived_test { cmint_key.as_ref(), &[params.vault_bump], ])?; ->>>>>>> a606eb113 (wip) CreateCTokenAtaCpi { payer: ctx.accounts.fee_payer.to_account_info(), @@ -255,35 +118,6 @@ pub mod csdk_anchor_full_derived_test { .invoke()?; } -<<<<<<< HEAD - let account_metas = config.to_account_metas(); - - // Serialize instruction data - let data = instruction_data.data().map_err(ProgramError::from)?; - - // Build mint action instruction - let mint_action_instruction = solana_program::instruction::Instruction { - program_id: light_token_interface::LIGHT_TOKEN_PROGRAM_ID.into(), - accounts: account_metas, - data, - }; - - let mut account_infos = cpi_accounts.to_account_infos(); - account_infos.push( - ctx.accounts - .compress_token_program_cpi_authority - .to_account_info(), - ); - account_infos.push(ctx.accounts.ctoken_program.to_account_info()); - account_infos.push(ctx.accounts.mint_authority.to_account_info()); - account_infos.push(ctx.accounts.mint_signer.to_account_info()); - account_infos.push(ctx.accounts.user.to_account_info()); - - invoke(&mint_action_instruction, &account_infos)?; - - user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; - game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; -======= if params.user_ata_mint_amount > 0 { CTokenMintToCpi { cmint: ctx.accounts.cmint.to_account_info(), @@ -295,7 +129,6 @@ pub mod csdk_anchor_full_derived_test { } .invoke()?; } ->>>>>>> a606eb113 (wip) Ok(()) } 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 8819784400..5da280ff24 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 @@ -1,17 +1,9 @@ -<<<<<<< HEAD -use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; -use csdk_anchor_full_derived_test::{ - AccountCreationData, CompressionParams, GameSession, UserRecord, -}; -use light_compressed_account::address::derive_address; -======= use anchor_lang::{InstructionData, ToAccountMetas}; use light_compressible::rent::SLOTS_PER_EPOCH; use light_compressible_client::{ get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, }; use light_ctoken_sdk::compressed_token::create_compressed_mint::find_cmint_address; ->>>>>>> a606eb113 (wip) use light_macros::pubkey; use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, @@ -299,97 +291,6 @@ async fn test_create_pdas_and_mint_auto() { .value .unwrap(); -<<<<<<< HEAD - assert_eq!( - compressed_game_session.address, - Some(game_compressed_address) - ); - assert!(compressed_game_session.data.is_some()); - - let game_buf = compressed_game_session.data.unwrap().data; - let game_session = GameSession::deserialize(&mut &game_buf[..]).unwrap(); - assert_eq!(game_session.session_id, session_id); - assert_eq!(game_session.game_type, "Complex Game"); - assert_eq!(game_session.player, payer.pubkey()); - assert_eq!(game_session.score, 0); - assert!(game_session.compression_info.is_none()); - - // Verify Light Token account - let spl_mint = find_mint_address(&mint_signer_pubkey).0; - let (_, token_account_address) = - csdk_anchor_full_derived_test::get_ctokensigner_seeds(&payer.pubkey(), &spl_mint); - - let ctoken_accounts = rpc - .get_compressed_token_accounts_by_owner(&token_account_address, None, None) - .await - .unwrap() - .value; - - assert!( - !ctoken_accounts.items.is_empty(), - "Should have compressed token accounts" - ); -} - -#[allow(clippy::too_many_arguments)] -pub async fn create_user_record_and_game_session( - rpc: &mut LightProgramTest, - user: &Keypair, - program_id: &Pubkey, - config_pda: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - authority: &Keypair, - mint_authority_keypair: &Keypair, - some_account: &Keypair, - session_id: u64, - category_id: u64, -) -> Pubkey { - let state_tree_info = rpc.get_random_state_tree_info().unwrap(); - - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new_with_cpi_context( - *program_id, - state_tree_info.cpi_context.unwrap(), - ); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let decimals = 6u8; - let mint_authority = mint_authority_keypair.pubkey(); - let freeze_authority = mint_authority; - let mint_signer = Keypair::new(); - let compressed_mint_address = - derive_mint_compressed_address(&mint_signer.pubkey(), &address_tree_pubkey); - - let (spl_mint, mint_bump) = find_mint_address(&mint_signer.pubkey()); - let accounts = csdk_anchor_full_derived_test::accounts::CreateUserRecordAndGameSession { - user: user.pubkey(), - mint_signer: mint_signer.pubkey(), - user_record: *user_record_pda, - game_session: *game_session_pda, - authority: authority.pubkey(), - mint_authority, - some_account: some_account.pubkey(), - ctoken_program: LIGHT_TOKEN_PROGRAM_ID.into(), - compress_token_program_cpi_authority: CPI_AUTHORITY_PDA.into(), - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_sponsor: RENT_SPONSOR, - }; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); -======= // Fetch compressed vault token account let compressed_vault_accounts = rpc .get_compressed_token_accounts_by_owner(&vault_pda, None, None) @@ -398,7 +299,6 @@ pub async fn create_user_record_and_game_session( .value .items; let compressed_vault = &compressed_vault_accounts[0]; ->>>>>>> a606eb113 (wip) // Get validity proof for PDAs + vault let rpc_result = rpc @@ -434,39 +334,6 @@ pub async fn create_user_record_and_game_session( authority: authority.pubkey(), session_id, }, -<<<<<<< HEAD - compression_params: CompressionParams { - proof: rpc_result.proof, - user_compressed_address, - user_address_tree_info, - user_output_state_tree_index, - game_compressed_address, - game_address_tree_info, - game_output_state_tree_index, - mint_bump, - mint_with_context: CompressedMintWithContext { - leaf_index: 0, - prove_by_index: false, - root_index: mint_address_tree_info.root_index, - address: compressed_mint_address, - mint: Some(CompressedMintInstructionData { - supply: 0, - decimals, - metadata: CompressedMintMetadata { - version: 3, - mint: spl_mint.into(), - cmint_decompressed: false, - mint_signer: mint_signer.pubkey().to_bytes(), - bump: mint_bump, - }, - mint_authority: Some(mint_authority.into()), - freeze_authority: Some(freeze_authority.into()), - extensions: None, - }), - }, - }, - }; -======= ) .expect("GameSession seed verification failed"), RentFreeDecompressAccount::from_ctoken( @@ -475,7 +342,6 @@ pub async fn create_user_record_and_game_session( ) .expect("CToken variant construction failed"), ]; ->>>>>>> a606eb113 (wip) // Build decompress instruction // No SeedParams needed - data.* seeds from unpacked account, ctx.* from variant idx diff --git a/sdk-tests/sdk-light-token-test/src/create_ata.rs b/sdk-tests/sdk-light-token-test/src/create_ata.rs index ab3f0ca660..a14a9a099f 100644 --- a/sdk-tests/sdk-light-token-test/src/create_ata.rs +++ b/sdk-tests/sdk-light-token-test/src/create_ata.rs @@ -1,9 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -<<<<<<< HEAD:sdk-tests/sdk-light-token-test/src/create_ata.rs -use light_token_sdk::token::{CompressibleParamsCpi, CreateAssociatedAccountCpi}; -======= use light_ctoken_sdk::ctoken::CreateCTokenAtaCpi; ->>>>>>> a606eb113 (wip):sdk-tests/sdk-ctoken-test/src/create_ata.rs use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; use crate::{ATA_SEED, ID}; @@ -34,20 +30,8 @@ pub fn process_create_ata_invoke( return Err(ProgramError::NotEnoughAccountKeys); } -<<<<<<< HEAD:sdk-tests/sdk-light-token-test/src/create_ata.rs - // Build the compressible params using constructor - let compressible_params = CompressibleParamsCpi::new_ata( - accounts[5].clone(), - accounts[6].clone(), - accounts[4].clone(), - ); - - // Use the CreateAssociatedCTokenAccountCpi - owner and mint are AccountInfos - CreateAssociatedAccountCpi { -======= CreateCTokenAtaCpi { payer: accounts[2].clone(), ->>>>>>> a606eb113 (wip):sdk-tests/sdk-ctoken-test/src/create_ata.rs owner: accounts[0].clone(), mint: accounts[1].clone(), ata: accounts[3].clone(), @@ -91,13 +75,8 @@ pub fn process_create_ata_invoke_signed( let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; -<<<<<<< HEAD:sdk-tests/sdk-light-token-test/src/create_ata.rs - // Use the CreateAssociatedAccountCpi - owner and mint are AccountInfos - let account_infos = CreateAssociatedAccountCpi { -======= CreateCTokenAtaCpi { payer: accounts[2].clone(), ->>>>>>> a606eb113 (wip):sdk-tests/sdk-ctoken-test/src/create_ata.rs owner: accounts[0].clone(), mint: accounts[1].clone(), ata: accounts[3].clone(), diff --git a/sdk-tests/sdk-light-token-test/src/create_token_account.rs b/sdk-tests/sdk-light-token-test/src/create_token_account.rs index 44badc6632..0301da922e 100644 --- a/sdk-tests/sdk-light-token-test/src/create_token_account.rs +++ b/sdk-tests/sdk-light-token-test/src/create_token_account.rs @@ -40,13 +40,8 @@ pub fn process_create_token_account_invoke( accounts[4].clone(), ); -<<<<<<< HEAD:sdk-tests/sdk-light-token-test/src/create_token_account.rs - // Build the account infos struct - CreateTokenAccountCpi { -======= // Build the account infos struct and invoke with custom compressible params CreateCTokenAccountCpi { ->>>>>>> a606eb113 (wip):sdk-tests/sdk-ctoken-test/src/create_token_account.rs payer: accounts[0].clone(), account: accounts[1].clone(), mint: accounts[2].clone(), @@ -89,14 +84,9 @@ pub fn process_create_token_account_invoke_signed( accounts[4].clone(), ); -<<<<<<< HEAD:sdk-tests/sdk-light-token-test/src/create_token_account.rs - // Build the account infos struct - let account_infos = CreateTokenAccountCpi { -======= // Invoke with PDA signing and custom compressible params let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; CreateCTokenAccountCpi { ->>>>>>> a606eb113 (wip):sdk-tests/sdk-ctoken-test/src/create_token_account.rs payer: accounts[0].clone(), account: accounts[1].clone(), mint: accounts[2].clone(), From 28c2eaea71e7d602d589a49c81fbac30214c50d1 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 16 Jan 2026 16:59:27 +0000 Subject: [PATCH 3/9] fix mc wip fixing fix 2 resolve more mcs fix fix - fix --- Cargo.lock | 6 +- Cargo.toml | 2 +- ctoken_for_payments.md | 8 +- .../compressed-token/program/docs/ACCOUNTS.md | 37 +- .../program/docs/ctoken/CREATE.md | 323 +++++---- .../compression/ctoken/compress_and_close.rs | 4 +- .../program/tests/compress_and_close.rs | 2 +- rebase.md | 95 +++ sdk-libs/compressible-client/Cargo.toml | 6 +- sdk-libs/compressible-client/DECOMPRESSION.md | 4 +- .../compressible-client/decompress-atas.md | 16 +- .../compressible-client/decompress-mint.md | 28 +- sdk-libs/compressible-client/decompress_ux.md | 22 +- sdk-libs/compressible-client/proof_helper.md | 4 +- .../src/create_accounts_proof.rs | 20 +- .../src/decompress_atas.rs | 50 +- .../src/decompress_mint.rs | 21 +- sdk-libs/compressible-client/src/lib.rs | 21 +- sdk-libs/macros/MACRO-NEW.md | 78 +-- sdk-libs/macros/MACRO_REFACTOR.md | 2 +- sdk-libs/macros/MACRO_REFACTOR_V2.md | 4 +- sdk-libs/macros/OPTION_A_PLAN.md | 24 +- sdk-libs/macros/OPTION_A_STATE_FLOW.md | 32 +- sdk-libs/macros/OVERVIEW.md | 6 +- sdk-libs/macros/SPEC_OPTION_A.md | 137 ++-- sdk-libs/macros/SPEC_OPTION_B.md | 135 ++-- sdk-libs/macros/src/compressible/README.md | 2 +- .../macros/src/compressible/anchor_seeds.rs | 120 +++- .../src/compressible/decompress_context.rs | 6 +- .../macros/src/compressible/instructions.rs | 662 +----------------- .../macros/src/compressible/seed_providers.rs | 105 ++- sdk-libs/macros/src/compressible/utils.rs | 4 +- .../macros/src/compressible/variant_enum.rs | 24 +- sdk-libs/macros/src/finalize/codegen.rs | 70 +- sdk-libs/macros/src/finalize/parse.rs | 2 +- sdk-libs/macros/src/lib.rs | 3 +- sdk-libs/program-test/src/compressible.rs | 16 +- .../src/program_test/light_program_test.rs | 47 +- .../src/compressible/decompress_runtime.rs | 13 +- sdk-libs/sdk/src/compressible/traits.rs | 6 +- .../src/compressible/decompress_runtime.rs | 97 +-- sdk-libs/token-sdk/src/pack.rs | 43 +- sdk-libs/token-sdk/src/token/create.rs | 32 +- sdk-libs/token-sdk/src/token/create_ata.rs | 2 +- .../token-sdk/src/token/decompress_mint.rs | 13 +- sdk-libs/token-sdk/src/token/mod.rs | 11 +- .../csdk-anchor-full-derived-test/SUMMARY.md | 38 +- .../src/instruction_accounts.rs | 4 +- .../csdk-anchor-full-derived-test/src/lib.rs | 6 +- .../src/state.rs | 10 - .../tests/basic_test.rs | 29 +- sdk-tests/sdk-light-token-test/README.md | 5 +- sdk-tests/sdk-light-token-test/src/approve.rs | 4 +- sdk-tests/sdk-light-token-test/src/burn.rs | 4 +- .../sdk-light-token-test/src/create_ata.rs | 2 +- .../src/create_token_account.rs | 4 +- sdk-tests/sdk-light-token-test/src/freeze.rs | 4 +- sdk-tests/sdk-light-token-test/src/revoke.rs | 4 +- sdk-tests/sdk-light-token-test/src/thaw.rs | 4 +- .../tests/test_approve_revoke.rs | 20 +- .../sdk-light-token-test/tests/test_burn.rs | 8 +- .../tests/test_create_ata.rs | 4 +- .../tests/test_ctoken_mint_to.rs | 8 +- .../tests/test_freeze_thaw.rs | 20 +- .../tests/test_transfer_checked.rs | 12 +- .../sdk-token-test/src/ctoken_pda/mod.rs | 2 +- .../sdk-token-test/src/pda_ctoken/mint.rs | 4 +- .../sdk-token-test/src/pda_ctoken/mod.rs | 2 +- sdk-tests/sdk-token-test/tests/ctoken_pda.rs | 2 +- sdk-tests/sdk-token-test/tests/pda_ctoken.rs | 2 +- 70 files changed, 997 insertions(+), 1570 deletions(-) create mode 100644 rebase.md diff --git a/Cargo.lock b/Cargo.lock index fc91d7e30d..8ce880c0c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3688,9 +3688,9 @@ dependencies = [ "light-client", "light-compressed-account", "light-compressible", - "light-ctoken-interface", - "light-ctoken-sdk", "light-sdk", + "light-token-interface", + "light-token-sdk", "solana-account", "solana-instruction", "solana-program", @@ -6012,7 +6012,7 @@ dependencies = [ ] [[package]] -name = "sdk-ctoken-test" +name = "sdk-light-token-test" version = "0.1.0" dependencies = [ "anchor-spl 0.31.1 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index 13dedafe14..a498d7a733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ members = [ "sdk-tests/sdk-native-test", "sdk-tests/sdk-v1-native-test", "sdk-tests/sdk-token-test", - "sdk-tests/sdk-ctoken-test", + "sdk-tests/sdk-light-token-test", "sdk-tests/csdk-anchor-full-derived-test", "forester-utils", "forester", diff --git a/ctoken_for_payments.md b/ctoken_for_payments.md index 696c6e5935..2cf501e8b3 100644 --- a/ctoken_for_payments.md +++ b/ctoken_for_payments.md @@ -76,7 +76,7 @@ import { createAssociatedTokenAccountInterfaceIdempotentInstruction, getAssociatedTokenAddressInterface, } from "@lightprotocol/compressed-token/unified"; -import { CTOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; +import { LIGHT_TOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; const ata = getAssociatedTokenAddressInterface(mint, recipient); @@ -86,7 +86,7 @@ const tx = new Transaction().add( ata, recipient, mint, - CTOKEN_PROGRAM_ID + LIGHT_TOKEN_PROGRAM_ID ) ); ``` @@ -203,7 +203,7 @@ import { getAssociatedTokenAddressInterface, createAssociatedTokenAccountInterfaceIdempotentInstruction, } from "@lightprotocol/compressed-token/unified"; -import { CTOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; +import { LIGHT_TOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; const destinationAta = getAssociatedTokenAddressInterface(mint, recipient); const createAtaIx = createAssociatedTokenAccountInterfaceIdempotentInstruction( @@ -211,7 +211,7 @@ const createAtaIx = createAssociatedTokenAccountInterfaceIdempotentInstruction( destinationAta, recipient, mint, - CTOKEN_PROGRAM_ID + LIGHT_TOKEN_PROGRAM_ID ); new Transaction().add(createAtaIx, transferIx); diff --git a/programs/compressed-token/program/docs/ACCOUNTS.md b/programs/compressed-token/program/docs/ACCOUNTS.md index 6df1a8941e..6e45fcc5af 100644 --- a/programs/compressed-token/program/docs/ACCOUNTS.md +++ b/programs/compressed-token/program/docs/ACCOUNTS.md @@ -1,4 +1,5 @@ # Accounts + - Compressed tokens can be decompressed to spl tokens. Spl tokens are not explicitly listed here. - **description** - **discriminator** @@ -8,11 +9,12 @@ - **derivation:** (only for pdas) - **associated instructions** (create, close, update) - ## Solana Accounts + - The compressed token program uses ### CToken + - **description** struct `CToken` ctoken solana account with spl token compatible state layout @@ -40,8 +42,9 @@ - **serialization example** borsh and zero copy deserialization deserialize the compressible extension, spl serialization only deserialize the base token data. zero copy: (always use in programs) + ```rust - use light_token_interface::state::ctoken::CToken; + use light_token_interface::state::token::CToken; use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut}; let (token, _) = CToken::zero_copy_at(&account_data)?; @@ -49,14 +52,16 @@ ``` borsh: (always use in client non solana program code) + ```rust use borsh::BorshDeserialize; - use light_token_interface::state::ctoken::CToken; + use light_token_interface::state::token::CToken; let token = CToken::deserialize(&mut &account_data[..])?; ``` spl serialization: (preferably use other serialization) + ```rust use spl_pod::bytemuck::pod_from_bytes; use spl_token_2022::pod::PodAccount; @@ -64,41 +69,43 @@ let pod_account = pod_from_bytes::(&account_data[..165])?; ``` - ### Associated CToken + - **description** struct `CToken` ctoken solana account with spl token compatible state layout - **derivation:** - seeds: [owner, ctoken_program_id, mint] + seeds: [owner, light_token_program_id, mint] - the same as `CToken` - ### Compressible Config + - owned by the LightRegistry program - defined in path `program-libs/compressible/src/config.rs` - crate: `light-compressible` - ## Compressed Accounts ### Compressed Token + - compressed token account. - version describes the hashing and the discriminator. (program-libs/token-interface/src/state/compressed_token/token_data_version.rs) - pub enum TokenDataVersion { - V1 = 1u8, // discriminator [2, 0, 0, 0, 0, 0, 0, 0], // 2 le (Poseidon hashed) - V2 = 2u8, // discriminator [0, 0, 0, 0, 0, 0, 0, 3], // 3 be (Poseidon hashed) - ShaFlat = 3u8, // discriminator [0, 0, 0, 0, 0, 0, 0, 4], // 4 be (Sha256 hash of borsh serialized data truncated to 31 bytes so that hash is less than be bn254 field size) - } + pub enum TokenDataVersion { + V1 = 1u8, // discriminator [2, 0, 0, 0, 0, 0, 0, 0], // 2 le (Poseidon hashed) + V2 = 2u8, // discriminator [0, 0, 0, 0, 0, 0, 0, 3], // 3 be (Poseidon hashed) + ShaFlat = 3u8, // discriminator [0, 0, 0, 0, 0, 0, 0, 4], // 4 be (Sha256 hash of borsh serialized data truncated to 31 bytes so that hash is less than be bn254 field size) + } ### Compressed Mint ## Extensions + The compressed token program supports multiple extensions defined in `program-libs/token-interface/src/state/extensions/`. ### Mint Extensions #### TokenMetadata + - Mint extension, compatible with TokenMetadata extension of Token2022. - Only available in compressed mints. - Path: `program-libs/token-interface/src/state/extensions/token_metadata.rs` @@ -106,27 +113,33 @@ The compressed token program supports multiple extensions defined in `program-li ### Token Account Extensions #### Compressible + - Token account extension, Token2022 does not have an equivalent extension. - Only available in ctoken solana accounts (decompressed ctokens), not in compressed token accounts. - Stores compression info (rent sponsor, config, creation slot, etc.) for rent management. - Path: `program-libs/token-interface/src/state/extensions/compressible.rs` #### CompressedOnly + - Marker extension indicating the account can only exist in compressed form. - Path: `program-libs/token-interface/src/state/extensions/compressed_only.rs` #### Pausable + - Token account extension compatible with Token2022 PausableAccount extension. - Path: `program-libs/token-interface/src/state/extensions/pausable.rs` #### PermanentDelegate + - Token account extension compatible with Token2022 PermanentDelegate extension. - Path: `program-libs/token-interface/src/state/extensions/permanent_delegate.rs` #### TransferFee + - Token account extension compatible with Token2022 TransferFee extension. - Path: `program-libs/token-interface/src/state/extensions/transfer_fee.rs` #### TransferHook + - Token account extension compatible with Token2022 TransferHook extension. - Path: `program-libs/token-interface/src/state/extensions/transfer_hook.rs` diff --git a/programs/compressed-token/program/docs/ctoken/CREATE.md b/programs/compressed-token/program/docs/ctoken/CREATE.md index ca6c9540df..31f04ce78d 100644 --- a/programs/compressed-token/program/docs/ctoken/CREATE.md +++ b/programs/compressed-token/program/docs/ctoken/CREATE.md @@ -1,34 +1,46 @@ - # Instructions + - This file documents create ctoken account and create associated ctoken account. ## 1. create ctoken account - **discriminator:** 18 - **enum:** `CTokenInstruction::CreateTokenAccount` - **path:** programs/compressed-token/program/src/ctoken/create.rs - - **description:** - 1. creates ctoken solana accounts with and without Compressible extension - 2. account layout `CToken` is defined in path: program-libs/token-interface/src/state/ctoken/ctoken_struct.rs - 3. extension layout `CompressionInfo` is defined in path: - program-libs/token-interface/src/state/extensions/compressible.rs - 4. A compressible token means that the ctoken solana account can be compressed by the rent authority as soon as the account balance is insufficient. - 5. Account creation without the compressible extension: +**discriminator:** 18 +**enum:** `CTokenInstruction::CreateTokenAccount` +**path:** programs/compressed-token/program/src/ctoken/create.rs + +**description:** + +1. creates ctoken solana accounts with and without Compressible extension +2. account layout `CToken` is defined in path: program-libs/token-interface/src/state/ctoken/ctoken_struct.rs +3. extension layout `CompressionInfo` is defined in path: + program-libs/token-interface/src/state/extensions/compressible.rs +4. A compressible token means that the ctoken solana account can be compressed by the rent authority as soon as the account balance is insufficient. +5. Account creation without the compressible extension: + + - Initializes an existing 165-byte solana account as a ctoken account (SPL-compatible size) - Only sets mint, owner, and state fields - no extension data - Account must already exist and be owned by the program - 6. Account creation with compressible extension: + +6. Account creation with compressible extension: + + - creates the ctoken account via cpi within the instruction, then initializes it. - expects a CompressibleConfig account to read the rent authority, rent recipient and RentConfig from. - if the payer is not the rent recipient the fee payer pays the rent and becomes the rent recipient (the rent recipient is a ctoken program pda that funds rent exemption for compressible ctoken solana accounts) - **Instruction data:** - 1. instruction data is defined in path: program-libs/token-interface/src/instructions/create_ctoken_account.rs +**Instruction data:** + +1. instruction data is defined in path: program-libs/token-interface/src/instructions/create_ctoken_account.rs + + - `owner`: The owner pubkey for the token account (32 bytes) - `compressible_config`: Optional `CompressibleExtensionInstructionData` (None = non-compressible account) - 2. Instruction data with compressible extension - program-libs/token-interface/src/instructions/extensions/compressible.rs + +2. Instruction data with compressible extension + program-libs/token-interface/src/instructions/extensions/compressible.rs + + - `token_account_version`: Version of the compressed token account hashing scheme (u8). Must be 3 (ShaFlat) - only version 3 is supported. - `rent_payment`: Number of epochs to prepay for rent (u8) - `rent_payment = 1` is explicitly forbidden to prevent epoch boundary timing edge case (its rent for the current rent epoch) @@ -38,168 +50,154 @@ - `write_top_up`: Additional lamports allocated for future write operations on the compressed account (u32) - `compress_to_account_pubkey`: Optional `CompressToPubkey` for compressing to account pubkey instead of owner - **Accounts:** - 1. token_account +**Accounts:** + +1. token_account + + - (signer for compressible, mutable) - The ctoken account being created - For compressible accounts: must be signer (account created via CPI) - For non-compressible accounts: doesn't need to be signer (SPL compatibility) - 2. mint + +2. mint + + - (non-mutable) - Mint pubkey is used for token account initialization and extension detection - Account is unchecked and doesn't need to be initialized, allowing compressed mints to be used without providing the compressed account - Optional accounts required to initialize ctoken account with compressible extension: - 3. payer - - (signer, mutable) - - User account, pays for the compression incentive when using rent_sponsor - 4. config - - (non-mutable) - - Owned by LightRegistry program, CompressibleConfig::discriminator matches - - Used to read RentConfig, rent_sponsor, and compression_authority - - Must be in ACTIVE state - 5. system_program - - (non-mutable) - - Required for account creation and rent transfer - 6. rent_payer - - (mutable) - - Either rent_sponsor PDA or custom fee payer - - If custom fee payer: must be signer, pays rent exemption + compression incentive - - If rent_sponsor: not signer, pays only rent exemption (payer pays compression incentive) - - **Instruction Logic and Checks:** - 1. Deserialize instruction data +Optional accounts required to initialize ctoken account with compressible extension: 3. payer - (signer, mutable) - User account, pays for the compression incentive when using rent_sponsor 4. config - (non-mutable) - Owned by LightRegistry program, CompressibleConfig::discriminator matches - Used to read RentConfig, rent_sponsor, and compression_authority - Must be in ACTIVE state 5. system_program - (non-mutable) - Required for account creation and rent transfer 6. rent_payer - (mutable) - Either rent_sponsor PDA or custom fee payer - If custom fee payer: must be signer, pays rent exemption + compression incentive - If rent_sponsor: not signer, pays only rent exemption (payer pays compression incentive) + +**Instruction Logic and Checks:** + +1. Deserialize instruction data + + - If instruction data len == 32 bytes, treat as owner-only (SPL Token initialize_account3 compatibility) - Otherwise, deserialize as `CreateTokenAccountInstructionData` - 2. Parse and check accounts based on is_compressible flag + +2. Parse and check accounts based on is_compressible flag + + - For compressible: token_account must be signer - 3. Check mint extensions using `has_mint_extensions()` - 4. If with compressible account: - 4.1. Parse payer, config, system_program, and rent_payer accounts - 4.2. Validate CompressibleConfig is active (not inactive or deprecated) - - Error: `CompressibleError::InvalidState` if not active - 4.3. If with compress_to_pubkey: - - Validates: derives address from provided seeds/bump and verifies it matches token_account pubkey - - Security: ensures account is a derivable PDA, preventing compression to non-signable addresses - 4.4. Validate compression_only requirement for restricted extensions: - - If mint has restricted extensions (e.g., TransferFee) and compression_only == 0 - - Error: `ErrorCode::CompressionOnlyRequired` - 4.5. Validate compression_only is only set for mints with restricted extensions: - - If compression_only != 0 and mint has no restricted extensions - - Error: `ErrorCode::CompressionOnlyNotAllowed` - 4.6. Validate rent_payment is not exactly 1 epoch (must cover more than the current rent epoch or be 0) - - Check: `compressible_config.rent_payment != 1` - - Error: `ErrorCode::OneEpochPrefundingNotAllowed` if validation fails - - Purpose: Prevent accounts from becoming immediately compressible due to epoch boundary timing - 4.7. Calculate account size based on mint extensions (includes Compressible extension) - 4.8. Calculate rent (rent exemption + prepaid epochs rent + compression incentive) - 4.9. Check whether rent_payer is custom fee payer (rent_payer != config.rent_sponsor) - 4.10. If custom rent payer: - - Verify rent_payer is signer (prevents executable accounts as rent_sponsor) - - Create account with custom rent_payer via CPI (pays both rent exemption + additional lamports) - 4.11. If using protocol rent_sponsor: - - Create account with rent_sponsor PDA as fee payer via CPI (pays only rent exemption) - - Transfer compression incentive to created ctoken account from payer via CPI - 4.12. `initialize_ctoken_account` (programs/compressed-token/program/src/shared/initialize_ctoken_account.rs) - - Build extensions Vec including Compressible extension and any mint extension markers - - Copy version from config (used to match config PDA version in subsequent instructions) - - If custom fee payer, set custom fee payer as ctoken account rent_sponsor - - Else set config.rent_sponsor as ctoken account rent_sponsor - - Set `last_claimed_slot` to current slot (tracks when rent was last claimed/initialized) - - Validate token_account_version == 3 (ShaFlat) - - Error: `ProgramError::InvalidInstructionData` if version != 3 - - Validate write_top_up <= config.rent_config.max_top_up - - Error: `CTokenError::WriteTopUpExceedsMaximum` if exceeded - - Validate mint account (if initialized): - - Check mint owner is SPL Token, Token-2022, or CToken program - - Error: `ProgramError::IncorrectProgramId` if invalid owner - - Check mint structure is valid (82 bytes for SPL, or has AccountType marker for T22) - - Error: `ProgramError::InvalidAccountData` if invalid structure - - Cache decimals from mint account in extension - 5. If without compressible account (non-compressible path): - 5.1. Validate mint does not have restricted extensions - - Check: `!mint_extensions.has_restricted_extensions()` - - Error: `ErrorCode::MissingCompressibleConfig` if mint has restricted extensions - - Rationale: Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) must be marked as compression_only, and that marker is part of the Compressible extension - - **Errors:** - - `ProgramError::BorshIoError` (error code: 15) - Failed to deserialize CreateTokenAccountInstructionData from instruction_data bytes - - `AccountError::NotEnoughAccountKeys` (error code: 12020) - Missing required accounts - - `AccountError::InvalidSigner` (error code: 12015) - token_account or payer is not a signer when required - - `AccountError::AccountNotMutable` (error code: 12008) - token_account or payer is not mutable when required - - `AccountError::AccountOwnedByWrongProgram` (error code: 12007) - Config account not owned by LightRegistry program - - `ProgramError::InvalidAccountData` (error code: 4) - CompressibleConfig pod deserialization fails, compress_to_pubkey.check_seeds() fails, or invalid mint structure - - `ProgramError::InvalidInstructionData` (error code: 3) - compressible_config is None in instruction data when compressible accounts provided, extension data invalid, or token_account_version != 3 - - `ProgramError::MissingRequiredSignature` (error code: 8) - Custom rent_payer is not a signer - - `ProgramError::UnsupportedSysvar` (error code: 17) - Failed to get Clock sysvar - - `ProgramError::IncorrectProgramId` (error code: 1) - Mint account owner is not SPL Token, Token-2022, or CToken program - - `CompressibleError::InvalidState` (error code: 19002) - CompressibleConfig is not in active state - - `ErrorCode::InsufficientAccountSize` (error code: 6077) - token_account data length < 165 bytes (non-compressible) or < COMPRESSIBLE_TOKEN_ACCOUNT_SIZE (compressible) - - `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6101) - rent_payment is exactly 1 epoch, which is forbidden due to epoch boundary timing edge case - - `ErrorCode::CompressionOnlyRequired` (error code: 6131) - Mint has restricted extensions (e.g., TransferFee) but compression_only is not set in instruction data - - `ErrorCode::MissingCompressibleConfig` (error code: 6115) - Either: (1) compressible_config is Some in instruction data but compressible accounts are missing, or (2) non-compressible account creation attempted for mint with restricted extensions - - `ErrorCode::CompressionOnlyNotAllowed` (error code: 6151) - compression_only is set but mint has no restricted extensions - - `CTokenError::WriteTopUpExceedsMaximum` (error code: 18042) - write_top_up exceeds config.rent_config.max_top_up - - `CTokenError::MissingCompressibleExtension` (error code: 18056) - Compressible extension initialization failed internally +3. Check mint extensions using `has_mint_extensions()` +4. If with compressible account: + 4.1. Parse payer, config, system_program, and rent_payer accounts + 4.2. Validate CompressibleConfig is active (not inactive or deprecated) - Error: `CompressibleError::InvalidState` if not active + 4.3. If with compress_to_pubkey: - Validates: derives address from provided seeds/bump and verifies it matches token_account pubkey - Security: ensures account is a derivable PDA, preventing compression to non-signable addresses + 4.4. Validate compression_only requirement for restricted extensions: - If mint has restricted extensions (e.g., TransferFee) and compression_only == 0 - Error: `ErrorCode::CompressionOnlyRequired` + 4.5. Validate compression_only is only set for mints with restricted extensions: - If compression_only != 0 and mint has no restricted extensions - Error: `ErrorCode::CompressionOnlyNotAllowed` + 4.6. Validate rent_payment is not exactly 1 epoch (must cover more than the current rent epoch or be 0) - Check: `compressible_config.rent_payment != 1` - Error: `ErrorCode::OneEpochPrefundingNotAllowed` if validation fails - Purpose: Prevent accounts from becoming immediately compressible due to epoch boundary timing + 4.7. Calculate account size based on mint extensions (includes Compressible extension) + 4.8. Calculate rent (rent exemption + prepaid epochs rent + compression incentive) + 4.9. Check whether rent_payer is custom fee payer (rent_payer != config.rent_sponsor) + 4.10. If custom rent payer: - Verify rent_payer is signer (prevents executable accounts as rent_sponsor) - Create account with custom rent_payer via CPI (pays both rent exemption + additional lamports) + 4.11. If using protocol rent_sponsor: - Create account with rent_sponsor PDA as fee payer via CPI (pays only rent exemption) - Transfer compression incentive to created ctoken account from payer via CPI + 4.12. `initialize_ctoken_account` (programs/compressed-token/program/src/shared/initialize_ctoken_account.rs) - Build extensions Vec including Compressible extension and any mint extension markers - Copy version from config (used to match config PDA version in subsequent instructions) - If custom fee payer, set custom fee payer as ctoken account rent_sponsor - Else set config.rent_sponsor as ctoken account rent_sponsor - Set `last_claimed_slot` to current slot (tracks when rent was last claimed/initialized) - Validate token_account_version == 3 (ShaFlat) - Error: `ProgramError::InvalidInstructionData` if version != 3 - Validate write_top_up <= config.rent_config.max_top_up - Error: `CTokenError::WriteTopUpExceedsMaximum` if exceeded - Validate mint account (if initialized): - Check mint owner is SPL Token, Token-2022, or CToken program - Error: `ProgramError::IncorrectProgramId` if invalid owner - Check mint structure is valid (82 bytes for SPL, or has AccountType marker for T22) - Error: `ProgramError::InvalidAccountData` if invalid structure - Cache decimals from mint account in extension +5. If without compressible account (non-compressible path): + 5.1. Validate mint does not have restricted extensions - Check: `!mint_extensions.has_restricted_extensions()` - Error: `ErrorCode::MissingCompressibleConfig` if mint has restricted extensions - Rationale: Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) must be marked as compression_only, and that marker is part of the Compressible extension + +**Errors:** + +- `ProgramError::BorshIoError` (error code: 15) - Failed to deserialize CreateTokenAccountInstructionData from instruction_data bytes +- `AccountError::NotEnoughAccountKeys` (error code: 12020) - Missing required accounts +- `AccountError::InvalidSigner` (error code: 12015) - token_account or payer is not a signer when required +- `AccountError::AccountNotMutable` (error code: 12008) - token_account or payer is not mutable when required +- `AccountError::AccountOwnedByWrongProgram` (error code: 12007) - Config account not owned by LightRegistry program +- `ProgramError::InvalidAccountData` (error code: 4) - CompressibleConfig pod deserialization fails, compress_to_pubkey.check_seeds() fails, or invalid mint structure +- `ProgramError::InvalidInstructionData` (error code: 3) - compressible_config is None in instruction data when compressible accounts provided, extension data invalid, or token_account_version != 3 +- `ProgramError::MissingRequiredSignature` (error code: 8) - Custom rent_payer is not a signer +- `ProgramError::UnsupportedSysvar` (error code: 17) - Failed to get Clock sysvar +- `ProgramError::IncorrectProgramId` (error code: 1) - Mint account owner is not SPL Token, Token-2022, or CToken program +- `CompressibleError::InvalidState` (error code: 19002) - CompressibleConfig is not in active state +- `ErrorCode::InsufficientAccountSize` (error code: 6077) - token_account data length < 165 bytes (non-compressible) or < COMPRESSIBLE_TOKEN_ACCOUNT_SIZE (compressible) +- `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6101) - rent_payment is exactly 1 epoch, which is forbidden due to epoch boundary timing edge case +- `ErrorCode::CompressionOnlyRequired` (error code: 6131) - Mint has restricted extensions (e.g., TransferFee) but compression_only is not set in instruction data +- `ErrorCode::MissingCompressibleConfig` (error code: 6115) - Either: (1) compressible_config is Some in instruction data but compressible accounts are missing, or (2) non-compressible account creation attempted for mint with restricted extensions +- `ErrorCode::CompressionOnlyNotAllowed` (error code: 6151) - compression_only is set but mint has no restricted extensions +- `CTokenError::WriteTopUpExceedsMaximum` (error code: 18042) - write_top_up exceeds config.rent_config.max_top_up +- `CTokenError::MissingCompressibleExtension` (error code: 18056) - Compressible extension initialization failed internally ## 2. create associated ctoken account - **discriminator:** 100 (non-idempotent), 102 (idempotent) - **enum:** `CTokenInstruction::CreateAssociatedTokenAccount` (non-idempotent), `CTokenInstruction::CreateAssociatedTokenAccountIdempotent` (idempotent) - **path:** programs/compressed-token/program/src/ctoken/create_ata.rs - - **description:** - 1. Creates deterministic ctoken PDA accounts derived from [owner, ctoken_program_id, mint] - 2. Supports both non-idempotent (fails if exists) and idempotent (succeeds if exists) modes - 3. Account layout same as create ctoken account: `CToken` with optional `CompressionInfo` - 4. Associated token accounts cannot use compress_to_pubkey (always compress to owner) - 5. Owner and mint are provided as accounts, bump is provided via instruction data - 6. Token account must be uninitialized (owned by system program) unless idempotent mode - 7. ATAs for mints with restricted extensions must be compressible (the compression_only marker is part of the Compressible extension) - - **Instruction data:** - 1. instruction data is defined in path: program-libs/token-interface/src/instructions/create_associated_token_account.rs +**discriminator:** 100 (non-idempotent), 102 (idempotent) +**enum:** `CTokenInstruction::CreateAssociatedTokenAccount` (non-idempotent), `CTokenInstruction::CreateAssociatedTokenAccountIdempotent` (idempotent) +**path:** programs/compressed-token/program/src/ctoken/create_ata.rs + +**description:** + +1. Creates deterministic ctoken PDA accounts derived from [owner, light_token_program_id, mint] +2. Supports both non-idempotent (fails if exists) and idempotent (succeeds if exists) modes +3. Account layout same as create ctoken account: `CToken` with optional `CompressionInfo` +4. Associated token accounts cannot use compress_to_pubkey (always compress to owner) +5. Owner and mint are provided as accounts, bump is provided via instruction data +6. Token account must be uninitialized (owned by system program) unless idempotent mode +7. ATAs for mints with restricted extensions must be compressible (the compression_only marker is part of the Compressible extension) + +**Instruction data:** + +1. instruction data is defined in path: program-libs/token-interface/src/instructions/create_associated_token_account.rs + + - `bump`: PDA bump seed for derivation (u8) - `compressible_config`: Optional `CompressibleExtensionInstructionData`, same as create ctoken account but: - `compress_to_account_pubkey` must be None (ATAs always compress to owner) - `compression_only` must be non-zero (compressible ATAs require compression_only) - **Accounts:** - 1. owner +**Accounts:** + +1. owner + + - (non-mutable, non-signer) - The owner of the associated token account (used for PDA derivation and initialization) - 2. mint + +2. mint + + - (non-mutable, non-signer) - The mint for the token account (used for PDA derivation and initialization) - 3. fee_payer + +3. fee_payer + + - (signer, mutable) - Pays for account creation and compression incentive - 4. associated_token_account + +4. associated_token_account + + - (mutable, NOT signer) - The PDA being created, must be system-owned (uninitialized) unless idempotent - 5. system_program + +5. system_program + + - (non-mutable) - Required for account creation - Optional accounts for compressible extension (same as create ctoken account): - 6. config - - (non-mutable) - - Owned by LightRegistry program, CompressibleConfig::discriminator matches - - Used to read RentConfig, rent_sponsor, and compression_authority - 7. rent_payer - - (mutable) - - Either rent_sponsor PDA or custom fee payer (must be signer if custom) - - **Instruction Logic and Checks:** - 1. Deserialize instruction data - 2. Parse accounts: owner, mint, fee_payer, associated_token_account, system_program - 3. If idempotent mode: +Optional accounts for compressible extension (same as create ctoken account): 6. config - (non-mutable) - Owned by LightRegistry program, CompressibleConfig::discriminator matches - Used to read RentConfig, rent_sponsor, and compression_authority 7. rent_payer - (mutable) - Either rent_sponsor PDA or custom fee payer (must be signer if custom) + +**Instruction Logic and Checks:** + +1. Deserialize instruction data +2. Parse accounts: owner, mint, fee_payer, associated_token_account, system_program +3. If idempotent mode: + + - Validate PDA derivation matches [owner, program_id, mint] with provided bump - Return success if account already owned by ctoken program - 4. Verify account is system-owned (uninitialized) + +4. Verify account is system-owned (uninitialized) + + - Error: `ProgramError::IllegalOwner` if not owned by system program - 5. If compressible: + +5. If compressible: + + - Reject if compress_to_account_pubkey is Some (not allowed for ATAs) - Error: `ProgramError::InvalidInstructionData` if compress_to_account_pubkey is Some - Validate compression_only is set (required for compressible ATAs) @@ -219,21 +217,20 @@ - If using protocol rent_sponsor: - Create ATA PDA with rent_sponsor PDA paying rent exemption - Transfer compression incentive from fee_payer to account via CPI - 6. If not compressible: - 6.1. Validate mint does not have restricted extensions - - Check: `!mint_extensions.has_restricted_extensions()` - - Error: `ErrorCode::MissingCompressibleConfig` if mint has restricted extensions - - Rationale: Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) must be marked as compression_only, and that marker is part of the Compressible extension - 6.2. Create ATA PDA with fee_payer paying rent exemption (base 165-byte SPL layout) - 7. Initialize token account with is_ata flag set (same as ## 1. create ctoken account step 4.12, but with is_ata=true) - - **Errors:** - Same as create ctoken account with additions: - - `ProgramError::IllegalOwner` (error code: 18) - Associated token account not owned by system program when creating - - `ProgramError::InvalidInstructionData` (error code: 3) - compress_to_account_pubkey is Some (forbidden for ATAs) - - `ProgramError::MissingRequiredSignature` (error code: 8) - Custom rent_payer is not a signer - - `AccountError::InvalidSigner` (error code: 12015) - fee_payer is not a signer - - `AccountError::AccountNotMutable` (error code: 12008) - fee_payer or associated_token_account is not mutable - - `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6101) - rent_payment is exactly 1 epoch (see create ctoken account errors) - - `ErrorCode::AtaRequiresCompressionOnly` (error code: 6152) - compressible ATA must have compression_only set (compression_only == 0 is not allowed) - - `ErrorCode::MissingCompressibleConfig` (error code: 6115) - non-compressible ATA creation attempted for mint with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) + +6. If not compressible: + 6.1. Validate mint does not have restricted extensions - Check: `!mint_extensions.has_restricted_extensions()` - Error: `ErrorCode::MissingCompressibleConfig` if mint has restricted extensions - Rationale: Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) must be marked as compression_only, and that marker is part of the Compressible extension + 6.2. Create ATA PDA with fee_payer paying rent exemption (base 165-byte SPL layout) +7. Initialize token account with is_ata flag set (same as ## 1. create ctoken account step 4.12, but with is_ata=true) + +**Errors:** +Same as create ctoken account with additions: + +- `ProgramError::IllegalOwner` (error code: 18) - Associated token account not owned by system program when creating +- `ProgramError::InvalidInstructionData` (error code: 3) - compress_to_account_pubkey is Some (forbidden for ATAs) +- `ProgramError::MissingRequiredSignature` (error code: 8) - Custom rent_payer is not a signer +- `AccountError::InvalidSigner` (error code: 12015) - fee_payer is not a signer +- `AccountError::AccountNotMutable` (error code: 12008) - fee_payer or associated_token_account is not mutable +- `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6101) - rent_payment is exactly 1 epoch (see create ctoken account errors) +- `ErrorCode::AtaRequiresCompressionOnly` (error code: 6152) - compressible ATA must have compression_only set (compression_only == 0 is not allowed) +- `ErrorCode::MissingCompressibleConfig` (error code: 6115) - non-compressible ATA creation attempted for mint with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs index c42c5dface..733249ba4e 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs @@ -20,7 +20,7 @@ use spl_pod::solana_msg::msg; use super::inputs::CompressAndCloseInputs; #[cfg(target_os = "solana")] -use crate::ctoken::close::accounts::CloseTokenAccountAccounts; +use crate::token::close::accounts::CloseTokenAccountAccounts; use crate::{ compressed_token::transfer2::accounts::Transfer2Accounts, shared::convert_program_error, }; @@ -272,7 +272,7 @@ pub fn close_for_compress_and_close( let authority = validated_accounts .packed_accounts .get_u8(compression.authority, "CompressAndClose: authority")?; - use crate::ctoken::close::processor::close_token_account; + use crate::token::close::processor::close_token_account; close_token_account(&CloseTokenAccountAccounts { token_account: token_account_info, destination, diff --git a/programs/compressed-token/program/tests/compress_and_close.rs b/programs/compressed-token/program/tests/compress_and_close.rs index aadb5c9160..8b0e6e4477 100644 --- a/programs/compressed-token/program/tests/compress_and_close.rs +++ b/programs/compressed-token/program/tests/compress_and_close.rs @@ -5,7 +5,7 @@ use light_account_checks::{ packed_accounts::ProgramPackedAccounts, }; use light_compressed_token::compressed_token::transfer2::{ - accounts::Transfer2Accounts, compression::ctoken::close_for_compress_and_close, + accounts::Transfer2Accounts, compression::token::close_for_compress_and_close, }; use light_token_interface::{ instructions::transfer2::{Compression, CompressionMode}, diff --git a/rebase.md b/rebase.md new file mode 100644 index 0000000000..8f1a57f45d --- /dev/null +++ b/rebase.md @@ -0,0 +1,95 @@ +# Rebase Resolution Notes + +## Context + +Rebasing `swen/clean-decompress-base` onto `main`. + +## Main Branch Changes (affecting this rebase) + +### 1. Package Renames + +- `ctoken-sdk` → `token-sdk` +- `light_token_sdk` → `light_token_sdk` +- `ctoken` module → `token` module in APIs +- `sdk-ctoken-test` → `sdk-light-token-test` +- `light_token_interface` → `light_token_interface` +- Type names: `CToken` → `Token` in some places + +### 2. Deleted Tests/Directories in Main + +Main deleted these that HEAD modified: + +- `sdk-tests/csdk-anchor-derived-test/` - Deleted in main +- `sdk-tests/sdk-compressible-test/` - Deleted in main +- `sdk-libs/macros/src/compressible/GUIDE.md` - Deleted in main + +### 3. User's Branch (HEAD) Changes to Preserve + +- **Phase 8 refactor**: `TokenSeedProvider` trait simplified - no accounts struct needed +- Seed pubkeys embedded directly in enum variants +- `HasTokenVariant::is_packed_token()` → `is_packed_ctoken()` in some places +- Various API simplifications in decompress_runtime + +## Resolution Decisions + +### Content Conflicts + +1. **sdk-libs/macros/src/compressible/instructions.rs** + - Keep user's Phase 8 changes (simplified API) + - Use main's naming (`light_token_sdk` not `light_token_sdk`) + - Resolution: Take HEAD's code, update package names to main's convention + +2. **sdk-libs/macros/src/compressible/decompress_context.rs** + - Keep user's `RentFreeAccountData` naming + - Use `light_token_sdk::compat::PackedCTokenData` (main's package name) + - Fix trait method naming to match + +3. **sdk-libs/macros/src/compressible/seed_providers.rs** + - Keep user's Phase 8 implementation (simpler trait) + - Update imports to `light_token_sdk` + +4. **sdk-libs/macros/src/compressible/variant_enum.rs** + - Keep user's variant structure with idx fields + - Use `light_token_sdk::compat::*` imports + +5. **sdk-libs/sdk/src/compressible/decompress_runtime.rs** + - Keep user's simplified `TokenSeedProvider` trait (no accounts struct) + - Update to `ctoken_program()` accessor name + +6. **sdk-libs/token-sdk/src/compressible/decompress_runtime.rs** + - Keep user's implementation with `TokenSeedProvider` re-export + - Fix variable names (`token_accounts` vs `ctoken_accounts`) + +7. **sdk-libs/token-sdk/src/pack.rs** + - Keep both main's and user's Pack impls (they're compatible) + - Use `light_token_interface` imports + +8. **sdk-libs/token-sdk/src/token/create.rs, create_ata.rs** + - Keep user's new builder pattern APIs + - Use main's `light_token_interface` imports + +9. **sdk-libs/program-test/src/compressible.rs** + - Keep user's `compression_only` field addition + - Use `CToken` type with zerocopy (main's approach for Token parsing) + +10. **Cargo.toml** + - Keep main's member list (sdk-light-token-test) + - Add back sdk-compressible-test and csdk-anchor-derived-test if still needed + - Resolution: User tests appear consolidated - use main's list + +### Modify/Delete Conflicts + +1. **sdk-tests/csdk-anchor-derived-test/\*** - DELETE (main removed, tests moved to csdk-anchor-full-derived-test) +2. **sdk-tests/sdk-compressible-test/\*** - DELETE (main removed, functionality consolidated) +3. **sdk-libs/macros/src/compressible/GUIDE.md** - DELETE (main removed docs) + +### Test Files + +- **csdk-anchor-full-derived-test** - Keep user's changes, update imports to `light_token_sdk` → `light_token_sdk` + +## Confidence Level + +- **High confidence**: Package renames are mechanical +- **High confidence**: Phase 8 API simplifications are the user's intended changes +- **Medium confidence**: Deleted test directories - assuming main's consolidation is correct +- **Note**: Variable naming (`token_accounts` vs `ctoken_accounts`) - using main's `token_` prefix consistently diff --git a/sdk-libs/compressible-client/Cargo.toml b/sdk-libs/compressible-client/Cargo.toml index f775c6e613..fb1a3980d9 100644 --- a/sdk-libs/compressible-client/Cargo.toml +++ b/sdk-libs/compressible-client/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/lightprotocol/light-protocol" description = "Client instruction builders for Light Protocol compressible accounts" [features] -anchor = ["anchor-lang", "light-sdk/anchor", "light-ctoken-sdk/anchor"] +anchor = ["anchor-lang", "light-sdk/anchor", "light-token-sdk/anchor"] [dependencies] solana-instruction = { workspace = true } @@ -19,8 +19,8 @@ spl-token-2022 = { workspace = true } light-client = { workspace = true, features = ["v2"] } light-sdk = { workspace = true, features = ["v2", "cpi-context"] } -light-ctoken-sdk = { workspace = true, features = ["cpi-context"] } -light-ctoken-interface = { workspace = true } +light-token-sdk = { workspace = true, features = ["cpi-context"] } +light-token-interface = { workspace = true } light-compressed-account = { workspace = true } light-compressible = { workspace = true } diff --git a/sdk-libs/compressible-client/DECOMPRESSION.md b/sdk-libs/compressible-client/DECOMPRESSION.md index e10b4239df..a167599ca7 100644 --- a/sdk-libs/compressible-client/DECOMPRESSION.md +++ b/sdk-libs/compressible-client/DECOMPRESSION.md @@ -21,7 +21,7 @@ let instructions = decompress_cmint(&mint, fee_payer, &rpc).await?; ## Unified Token Data `AtaInterface` always provides `token_data` regardless of hot/cold state. -Uses the standard `TokenData` type from `light_ctoken_sdk::compat`: +Uses the standard `TokenData` type from `light_token_sdk::compat`: ```rust let ata = rpc.get_ata_interface(&mint, &owner).await?; @@ -150,7 +150,7 @@ pub struct AtaInterface { pub decompression: Option, // If cold } -// Standard TokenData from light_ctoken_sdk::compat (re-exported) +// Standard TokenData from light_token_sdk::compat (re-exported) pub struct TokenData { pub mint: Pubkey, pub owner: Pubkey, // Note: for ATAs, this is the ATA pubkey diff --git a/sdk-libs/compressible-client/decompress-atas.md b/sdk-libs/compressible-client/decompress-atas.md index 7346f09292..f713b8f3f9 100644 --- a/sdk-libs/compressible-client/decompress-atas.md +++ b/sdk-libs/compressible-client/decompress-atas.md @@ -27,7 +27,7 @@ When a CToken ATA is auto-compressed: When querying the indexer: - Query by `owner = ATA_pubkey` (not wallet owner) -- ATA pubkey = `derive_ctoken_ata(wallet_owner, mint)` = PDA of `[wallet_owner, CTOKEN_PROGRAM_ID, mint]` +- ATA pubkey = `derive_ctoken_ata(wallet_owner, mint)` = PDA of `[wallet_owner, LIGHT_TOKEN_PROGRAM_ID, mint]` When decompressing: @@ -129,14 +129,14 @@ The implementation follows the same pattern as `DecompressToCtoken::instruction( use light_client::indexer::{CompressedTokenAccount, Indexer, ValidityProofWithContext}; use light_compressed_account::compressed_account::PackedMerkleContext; -use light_ctoken_interface::{ +use light_token_interface::{ instructions::{ extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, transfer2::MultiInputTokenDataWithContext, }, state::{ExtensionStruct, TokenDataVersion}, }; -use light_ctoken_sdk::{ +use light_token_sdk::{ compressed_token::{ v2::transfer2::{ create_transfer2_instruction, Transfer2AccountsMetaConfig, Transfer2Config, @@ -358,7 +358,7 @@ fn build_batch_decompress_instruction( #[derive(Debug)] pub enum CompressibleClientError { Indexer(light_client::indexer::IndexerError), - CTokenSdk(light_ctoken_sdk::error::CTokenSdkError), + CTokenSdk(light_token_sdk::error::CTokenSdkError), NoStateTreesInProof, ProgramError(solana_program_error::ProgramError), } @@ -369,8 +369,8 @@ impl From for CompressibleClientError { } } -impl From for CompressibleClientError { - fn from(e: light_ctoken_sdk::error::CTokenSdkError) -> Self { +impl From for CompressibleClientError { + fn from(e: light_token_sdk::error::CTokenSdkError) -> Self { Self::CTokenSdk(e) } } @@ -586,8 +586,8 @@ async fn test_decompress_ata_idempotent() { # In sdk-libs/compressible-client/Cargo.toml [dependencies] light-client = { path = "../client" } -light-ctoken-sdk = { path = "../ctoken-sdk" } -light-ctoken-interface = { path = "../../program-libs/ctoken-interface" } +light-token-sdk = { path = "../ctoken-sdk" } +light-token-interface = { path = "../../program-libs/ctoken-interface" } light-compressed-account = { path = "../../program-libs/compressed-account" } light-sdk = { path = "../sdk" } solana-pubkey = "2" diff --git a/sdk-libs/compressible-client/decompress-mint.md b/sdk-libs/compressible-client/decompress-mint.md index ce196a4a27..744358fabd 100644 --- a/sdk-libs/compressible-client/decompress-mint.md +++ b/sdk-libs/compressible-client/decompress-mint.md @@ -8,7 +8,7 @@ SDK-only functionality to decompress compressed CMint accounts (mints that were ### Can we build this purely in SDK without macro changes? -**YES**. This is fully supported by the existing `DecompressCMint` instruction builder in `ctoken-sdk`. +**YES**. This is fully supported by the existing `DecompressMint` instruction builder in `ctoken-sdk`. **Why:** @@ -16,7 +16,7 @@ SDK-only functionality to decompress compressed CMint accounts (mints that were 2. **mint_seed does NOT need to sign** - uses `with_mint_signer_no_sign()` internally. The mint_seed is only used for PDA derivation. -3. `DecompressCMint` struct already exists in `ctoken-sdk/src/ctoken/decompress_cmint.rs` with a complete `instruction()` method. +3. `DecompressMint` struct already exists in `ctoken-sdk/src/ctoken/decompress_cmint.rs` with a complete `instruction()` method. 4. All data needed is queryable from the indexer via the compressed mint's address. @@ -49,7 +49,7 @@ ZAction::DecompressMint(decompress_action) => { When a mint is created via `#[compressible]` macro (like in `csdk-anchor-full-derived-test`): 1. **mint_seed_pubkey** = A program PDA (e.g., `LP_MINT_SIGNER_SEED + authority`) -2. **CMint PDA** = `find_cmint_address(mint_seed_pubkey)` = PDA of `[COMPRESSED_MINT_SEED, mint_seed_pubkey]` under ctoken program +2. **CMint PDA** = `find_mint_address(mint_seed_pubkey)` = PDA of `[COMPRESSED_MINT_SEED, mint_seed_pubkey]` under ctoken program 3. **Compressed address** = `derive_cmint_compressed_address(mint_seed_pubkey, address_tree)` When querying the indexer: @@ -77,8 +77,8 @@ Unlike PDAs which require program signing for decompression, CMint decompression | Component | Location | Reuse | | ------------------------------------- | -------------------------------------------------------------------------- | ------ | -| `DecompressCMint` struct | `ctoken-sdk/src/ctoken/decompress_cmint.rs` | Direct | -| `find_cmint_address` | `ctoken-sdk/src/ctoken/create_cmint.rs` | Direct | +| `DecompressMint` struct | `ctoken-sdk/src/ctoken/decompress_cmint.rs` | Direct | +| `find_mint_address` | `ctoken-sdk/src/ctoken/create_cmint.rs` | Direct | | `derive_cmint_compressed_address` | `ctoken-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs` | Direct | | `CompressedMintWithContext` | `ctoken-interface/src/instructions/mint_action/instruction_data.rs` | Direct | | `get_compressed_account` | `light-client/src/indexer/indexer_trait.rs` | Direct | @@ -173,13 +173,13 @@ Note: `AlreadyDecompressed` is NOT an error - returns empty vec instead (idempot ```rust use light_client::indexer::{Indexer, IndexerError}; use light_compressed_account::instruction_data::compressed_proof::ValidityProof; -use light_ctoken_interface::{ +use light_token_interface::{ instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, state::CompressedMint, CMINT_ADDRESS_TREE, }; -use light_ctoken_sdk::ctoken::{ - derive_cmint_compressed_address, DecompressCMint, +use light_token_sdk::token::{ + derive_cmint_compressed_address, DecompressMint, }; use solana_instruction::Instruction; use solana_program_error::ProgramError; @@ -316,8 +316,8 @@ pub async fn decompress_mint_idempotent( mint: Some(mint_instruction_data), }; - // 8. Build DecompressCMint instruction - let decompress = DecompressCMint { + // 8. Build DecompressMint instruction + let decompress = DecompressMint { mint_seed_pubkey: request.mint_seed_pubkey, payer: fee_payer, authority: fee_payer, // Permissionless - any signer works @@ -383,7 +383,7 @@ parse_compressed_mint_data() -> CompressedMint { metadata.cmint_decompressed? } indexer.get_validity_proof([hash]) -> ValidityProofWithContext | v -DecompressCMint { +DecompressMint { mint_seed_pubkey, payer: fee_payer, authority: fee_payer, // Permissionless! @@ -444,8 +444,8 @@ if !instructions.is_empty() { # In sdk-libs/compressible-client/Cargo.toml [dependencies] light-client = { path = "../client" } -light-ctoken-sdk = { path = "../ctoken-sdk" } -light-ctoken-interface = { path = "../../program-libs/ctoken-interface" } +light-token-sdk = { path = "../ctoken-sdk" } +light-token-interface = { path = "../../program-libs/ctoken-interface" } light-compressed-account = { path = "../../program-libs/compressed-account" } solana-pubkey = "2" solana-instruction = "2" @@ -477,7 +477,7 @@ async fn test_decompress_mint() { &[LP_MINT_SIGNER_SEED, authority.pubkey().as_ref()], &program_id, ); - let (cmint_pda, _) = find_cmint_address(&mint_signer_pda); + let (cmint_pda, _) = find_mint_address(&mint_signer_pda); // ... execute create_pdas_and_mint_auto ... diff --git a/sdk-libs/compressible-client/decompress_ux.md b/sdk-libs/compressible-client/decompress_ux.md index 3122f32dfd..479e63c44f 100644 --- a/sdk-libs/compressible-client/decompress_ux.md +++ b/sdk-libs/compressible-client/decompress_ux.md @@ -19,6 +19,7 @@ RentFreeDecompressAccount::new(user_interface, user_variant) ``` **Issues:** + - 3 separate steps per account - `compressed_data()` extraction is boilerplate - Interface passed twice conceptually (once for data, once for wrapper) @@ -50,6 +51,7 @@ pub trait IntoVariant { ``` This trait is SBF-compatible because: + - No client-crate dependencies - Just takes `&[u8]` and returns variant - Lives in `light-sdk` which is already program-side @@ -59,6 +61,7 @@ This trait is SBF-compatible because: Location: `sdk-libs/macros/src/compressible/instructions.rs` Currently generates: + ```rust pub struct UserRecordSeeds { pub authority: Pubkey, @@ -75,6 +78,7 @@ impl CompressedAccountVariant { ``` **Add trait impl:** + ```rust impl light_sdk::compressible::IntoVariant for UserRecordSeeds { fn into_variant(self, data: &[u8]) -> Result { @@ -136,13 +140,14 @@ impl RentFreeDecompressAccount { ``` **Trait (generated by macro):** + ```rust pub trait IntoCTokenVariant { fn into_ctoken_variant(self, token_data: TokenData) -> V; } // Generated by macro -impl IntoCTokenVariant for CTokenAccountVariant { +impl IntoCTokenVariant for TokenAccountVariant { fn into_ctoken_variant(self, token_data: TokenData) -> CompressedAccountVariant { CompressedAccountVariant::CTokenData(CTokenData { variant: self, @@ -153,16 +158,18 @@ impl IntoCTokenVariant for CTokenAccountVariant { ``` Usage: + ```rust RentFreeDecompressAccount::from_ctoken( AccountInterface::cold(vault_pda, compressed_vault.account), - CTokenAccountVariant::Vault { cmint: cmint_pda }, + TokenAccountVariant::Vault { cmint: cmint_pda }, )? ``` ## Final API ### Before (verbose) + ```rust let user_interface = AccountInterface::cold(user_record_pda, compressed_user.clone()); let game_interface = AccountInterface::cold(game_session_pda, compressed_game.clone()); @@ -177,7 +184,7 @@ let game_variant = CompressedAccountVariant::game_session( GameSessionSeeds { user, authority, session_id }, )?; let vault_ctoken_data = CTokenData { - variant: CTokenAccountVariant::Vault { cmint: cmint_pda }, + variant: TokenAccountVariant::Vault { cmint: cmint_pda }, token_data: compressed_vault.token.clone(), }; @@ -189,6 +196,7 @@ let decompress_accounts = vec![ ``` ### After (clean) + ```rust let decompress_accounts = vec![ RentFreeDecompressAccount::from_seeds( @@ -201,7 +209,7 @@ let decompress_accounts = vec![ )?, RentFreeDecompressAccount::from_ctoken( AccountInterface::cold(vault_pda, compressed_vault.account), - CTokenAccountVariant::Vault { cmint: cmint_pda }, + TokenAccountVariant::Vault { cmint: cmint_pda }, )?, ]; ``` @@ -226,7 +234,7 @@ let decompress_accounts = vec![ 4. **Update macro to generate `IntoCTokenVariant` impl** - Same file - - For `CTokenAccountVariant` + - For `TokenAccountVariant` 5. **Add `from_seeds` method to `RentFreeDecompressAccount`** - File: `sdk-libs/compressible-client/src/lib.rs` @@ -250,16 +258,19 @@ let decompress_accounts = vec![ Both methods return `Result` because: **`from_seeds`:** + 1. `compressed_data()` might be `None` (hot account passed to cold-only method) 2. `into_variant()` can fail (seed verification, deserialization) **`from_ctoken`:** + 1. `compressed_data()` might be `None` (hot account passed) 2. `TokenData::try_from_slice()` can fail (malformed data) ## Rating: 9/10 ### Pros + - **Consistent**: Both use `AccountInterface` first arg - **Minimal**: Single call per account, no intermediate vars - **Type-safe**: Traits enforce correct mapping @@ -267,6 +278,7 @@ Both methods return `Result` because: - **Clear intent**: `from_seeds` vs `from_ctoken` ### Cons + - CToken still needs `.account` extraction from `CompressedTokenAccount` - Re-parses `TokenData` from bytes (indexer already parsed, but keeps API uniform) - Two traits to maintain (hidden from user) diff --git a/sdk-libs/compressible-client/proof_helper.md b/sdk-libs/compressible-client/proof_helper.md index 690b871013..db0024a65f 100644 --- a/sdk-libs/compressible-client/proof_helper.md +++ b/sdk-libs/compressible-client/proof_helper.md @@ -52,7 +52,7 @@ pub enum CreateAccountsProofInput { Pda(Pubkey), /// PDA with explicit owner (for cross-program accounts) PdaWithOwner { pda: Pubkey, owner: Pubkey }, - /// CMint (always uses CTOKEN_PROGRAM_ID internally) + /// CMint (always uses LIGHT_TOKEN_PROGRAM_ID internally) Mint(Pubkey), } @@ -338,7 +338,7 @@ let instruction = Instruction { ```rust use light_client::indexer::{Indexer, IndexerError, AddressWithTree}; use light_client::rpc::{Rpc, RpcError}; -use light_ctoken_sdk::compressed_token::create_compressed_mint::derive_cmint_compressed_address; +use light_token_sdk::compressed_token::create_compressed_mint::derive_cmint_compressed_address; use light_compressed_account::address::derive_address; use light_sdk::instruction::{PackedAddressTreeInfo, ValidityProof}; use crate::pack::{pack_proof, PackError}; diff --git a/sdk-libs/compressible-client/src/create_accounts_proof.rs b/sdk-libs/compressible-client/src/create_accounts_proof.rs index 33812c89e7..81e04faa71 100644 --- a/sdk-libs/compressible-client/src/create_accounts_proof.rs +++ b/sdk-libs/compressible-client/src/create_accounts_proof.rs @@ -8,7 +8,7 @@ use light_client::indexer::{AddressWithTree, Indexer, IndexerError}; use light_client::rpc::{Rpc, RpcError}; -use light_ctoken_sdk::compressed_token::create_compressed_mint::derive_cmint_compressed_address; +use light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address; use solana_instruction::AccountMeta; use solana_pubkey::Pubkey; use thiserror::Error; @@ -39,7 +39,7 @@ pub enum CreateAccountsProofInput { Pda(Pubkey), /// PDA with explicit owner (for cross-program accounts) PdaWithOwner { pda: Pubkey, owner: Pubkey }, - /// CMint (always uses CTOKEN_PROGRAM_ID internally) + /// CMint (always uses LIGHT_TOKEN_PROGRAM_ID internally) Mint(Pubkey), } @@ -57,7 +57,7 @@ impl CreateAccountsProofInput { } /// Compressed mint (CMint). - /// Address derived: `derive_cmint_compressed_address(&mint_signer, &tree)` + /// Address derived: `derive_mint_compressed_address(&mint_signer, &tree)` pub fn mint(mint_signer: Pubkey) -> Self { Self::Mint(mint_signer) } @@ -70,14 +70,12 @@ impl CreateAccountsProofInput { &address_tree.to_bytes(), &program_id.to_bytes(), ), - Self::PdaWithOwner { pda, owner } => { - light_compressed_account::address::derive_address( - &pda.to_bytes(), - &address_tree.to_bytes(), - &owner.to_bytes(), - ) - } - Self::Mint(signer) => derive_cmint_compressed_address(signer, address_tree), + Self::PdaWithOwner { pda, owner } => light_compressed_account::address::derive_address( + &pda.to_bytes(), + &address_tree.to_bytes(), + &owner.to_bytes(), + ), + Self::Mint(signer) => derive_mint_compressed_address(signer, address_tree), } } } diff --git a/sdk-libs/compressible-client/src/decompress_atas.rs b/sdk-libs/compressible-client/src/decompress_atas.rs index 2bdb4cf3e1..623c58c2a1 100644 --- a/sdk-libs/compressible-client/src/decompress_atas.rs +++ b/sdk-libs/compressible-client/src/decompress_atas.rs @@ -27,16 +27,17 @@ use light_client::indexer::{ CompressedTokenAccount, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, IndexerError, ValidityProofWithContext, }; -use light_ctoken_sdk::compat::TokenData; use light_compressed_account::compressed_account::PackedMerkleContext; -use light_ctoken_interface::{ +use light_sdk::instruction::PackedAccounts; +use light_token_interface::{ instructions::{ extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, transfer2::MultiInputTokenDataWithContext, }, state::{ExtensionStruct, TokenDataVersion}, }; -use light_ctoken_sdk::{ +use light_token_sdk::compat::TokenData; +use light_token_sdk::{ compat::AccountState, compressed_token::{ transfer2::{ @@ -45,10 +46,9 @@ use light_ctoken_sdk::{ }, CTokenAccount2, }, - ctoken::{derive_ctoken_ata, CreateAssociatedCTokenAccount}, - error::CTokenSdkError, + error::TokenSdkError, + token::{derive_token_ata, CreateAssociatedTokenAccount}, }; -use light_sdk::instruction::PackedAccounts; use solana_account::Account; use solana_instruction::Instruction; use solana_program_error::ProgramError; @@ -62,8 +62,8 @@ pub enum DecompressAtaError { #[error("Indexer error: {0}")] Indexer(#[from] IndexerError), - #[error("CToken SDK error: {0}")] - CTokenSdk(#[from] CTokenSdkError), + #[error("Token SDK error: {0}")] + TokenSdk(#[from] TokenSdkError), #[error("No state trees in proof")] NoStateTreesInProof, @@ -367,11 +367,11 @@ pub fn build_decompress_token_accounts( for token_account in token_accounts.iter() { if let Some(ctx) = &token_account.decompression_context { // Derive ATA for destination - let (ata_pubkey, _) = derive_ctoken_ata(&ctx.wallet_owner, &ctx.mint); + let (ata_pubkey, _) = derive_token_ata(&ctx.wallet_owner, &ctx.mint); // Create ATA idempotently let create_ata = - CreateAssociatedCTokenAccount::new(fee_payer, ctx.wallet_owner, ctx.mint) + CreateAssociatedTokenAccount::new(fee_payer, ctx.wallet_owner, ctx.mint) .idempotent() .instruction()?; create_ata_instructions.push(create_ata); @@ -452,10 +452,9 @@ pub fn build_decompress_atas( for ata in atas.iter() { if ata.is_cold { if let Some(decompression) = &ata.decompression { - let create_ata = - CreateAssociatedCTokenAccount::new(fee_payer, ata.owner, ata.mint) - .idempotent() - .instruction()?; + let create_ata = CreateAssociatedTokenAccount::new(fee_payer, ata.owner, ata.mint) + .idempotent() + .instruction()?; create_ata_instructions.push(create_ata); cold_contexts.push(InternalAtaDecompressContext { @@ -530,12 +529,12 @@ pub async fn decompress_atas_idempotent( // Phase 1: Gather compressed token accounts and prepare ATA creation for (mint, wallet_owner) in mint_owner_pairs { - let (ata_pubkey, ata_bump) = derive_ctoken_ata(wallet_owner, mint); + let (ata_pubkey, ata_bump) = derive_token_ata(wallet_owner, mint); // Query compressed tokens owned by this ATA - let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new(Some( - *mint, - ))); + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new( + Some(*mint), + )); let result = indexer .get_compressed_token_accounts_by_owner(&ata_pubkey, options, None) .await?; @@ -546,7 +545,7 @@ pub async fn decompress_atas_idempotent( } // Create ATA idempotently - let create_ata = CreateAssociatedCTokenAccount::new(fee_payer, *wallet_owner, *mint) + let create_ata = CreateAssociatedTokenAccount::new(fee_payer, *wallet_owner, *mint) .idempotent() .instruction()?; create_ata_instructions.push(create_ata); @@ -572,7 +571,10 @@ pub async fn decompress_atas_idempotent( .map(|ctx| ctx.token_account.account.hash) .collect(); - let proof_result = indexer.get_validity_proof(hashes, vec![], None).await?.value; + let proof_result = indexer + .get_validity_proof(hashes, vec![], None) + .await? + .value; // Phase 3: Build decompress instruction let decompress_ix = build_batch_decompress_instruction(fee_payer, &all_accounts, proof_result)?; @@ -643,7 +645,7 @@ fn build_batch_decompress_instruction( // Build CTokenAccount2 for decompress let mut ctoken_account = CTokenAccount2::new(vec![source])?; - ctoken_account.decompress_ctoken(token.amount, destination_index)?; + ctoken_account.decompress(token.amount, destination_index)?; token_accounts_vec.push(ctoken_account); // Build TLV for this input (CompressedOnly extension for ATAs) @@ -706,7 +708,7 @@ mod tests { fn test_derive_ata() { let wallet = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let (ata, bump) = derive_ctoken_ata(&wallet, &mint); + let (ata, bump) = derive_token_ata(&wallet, &mint); assert_ne!(ata, wallet); assert_ne!(ata, mint); let _ = bump; @@ -716,7 +718,7 @@ mod tests { fn test_ata_interface_is_cold() { let wallet = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let (ata, bump) = derive_ctoken_ata(&wallet, &mint); + let (ata, bump) = derive_token_ata(&wallet, &mint); let hot_ata = AtaInterface { ata, @@ -758,7 +760,7 @@ mod tests { fn test_build_decompress_atas_fast_exit() { let wallet = Pubkey::new_unique(); let mint = Pubkey::new_unique(); - let (ata, bump) = derive_ctoken_ata(&wallet, &mint); + let (ata, bump) = derive_token_ata(&wallet, &mint); // All hot - should return empty vec let hot_atas = vec![AtaInterface { diff --git a/sdk-libs/compressible-client/src/decompress_mint.rs b/sdk-libs/compressible-client/src/decompress_mint.rs index 90b145217a..0301260073 100644 --- a/sdk-libs/compressible-client/src/decompress_mint.rs +++ b/sdk-libs/compressible-client/src/decompress_mint.rs @@ -15,12 +15,13 @@ use borsh::BorshDeserialize; use light_client::indexer::{CompressedAccount, Indexer, IndexerError, ValidityProofWithContext}; use light_compressed_account::instruction_data::compressed_proof::ValidityProof; -use light_ctoken_interface::{ +use light_token_interface::{ instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, state::CompressedMint, CMINT_ADDRESS_TREE, }; -use light_ctoken_sdk::ctoken::{derive_cmint_compressed_address, find_cmint_address, DecompressCMint}; +use light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address; +use light_token_sdk::token::{find_mint_address, DecompressMint}; use solana_account::Account; use solana_instruction::Instruction; use solana_program_error::ProgramError; @@ -185,9 +186,8 @@ pub fn build_decompress_mint( mint: Some(mint_instruction_data), }; - // Build DecompressCMint instruction - let decompress = DecompressCMint { - mint_seed_pubkey: mint.signer, + // Build DecompressMint instruction + let decompress = DecompressMint { payer: fee_payer, authority: fee_payer, // Permissionless - any signer works state_tree, @@ -316,7 +316,7 @@ pub async fn decompress_mint_idempotent( .address_tree .unwrap_or(Pubkey::new_from_array(CMINT_ADDRESS_TREE)); let compressed_address = - derive_cmint_compressed_address(&request.mint_seed_pubkey, &address_tree); + derive_mint_compressed_address(&request.mint_seed_pubkey, &address_tree); // 2. Fetch compressed mint account from indexer let compressed_account = indexer @@ -374,9 +374,8 @@ pub async fn decompress_mint_idempotent( mint: Some(mint_instruction_data), }; - // 8. Build DecompressCMint instruction - let decompress = DecompressCMint { - mint_seed_pubkey: request.mint_seed_pubkey, + // 8. Build DecompressMint instruction + let decompress = DecompressMint { payer: fee_payer, authority: fee_payer, // Permissionless - any signer works state_tree, @@ -402,8 +401,8 @@ pub fn create_mint_interface( onchain_account: Option, compressed: Option<(CompressedAccount, CompressedMint)>, ) -> MintInterface { - let (cmint, _) = find_cmint_address(&signer); - let compressed_address = derive_cmint_compressed_address(&signer, &address_tree); + let (cmint, _) = find_mint_address(&signer); + let compressed_address = derive_mint_compressed_address(&signer, &address_tree); let state = if let Some(account) = onchain_account { MintState::Hot { account } diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs index d073aa2c12..59831dcc8e 100644 --- a/sdk-libs/compressible-client/src/lib.rs +++ b/sdk-libs/compressible-client/src/lib.rs @@ -35,7 +35,7 @@ pub use decompress_mint::{ MintState, DEFAULT_RENT_PAYMENT, DEFAULT_WRITE_TOP_UP, }; pub use initialize_config::InitializeRentFreeConfig; -pub use light_ctoken_sdk::compat::TokenData; +pub use light_token_sdk::compat::TokenData; pub use pack::{pack_proof, PackError, PackedProofResult}; #[cfg(feature = "anchor")] @@ -43,18 +43,17 @@ use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; use light_client::indexer::{CompressedAccount, TreeInfo, ValidityProofWithContext}; -use light_ctoken_sdk::ctoken::{ - COMPRESSIBLE_CONFIG_V1, CTOKEN_CPI_AUTHORITY, CTOKEN_PROGRAM_ID, RENT_SPONSOR, -}; pub use light_sdk::compressible::config::CompressibleConfig; use light_sdk::{ compressible::{compression_info::CompressedAccountData, Pack}, - constants::LIGHT_TOKEN_PROGRAM_ID, instruction::{ account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, SystemAccountMetaConfig, ValidityProof, }, }; +use light_token_sdk::token::{ + COMPRESSIBLE_CONFIG_V1, LIGHT_TOKEN_CPI_AUTHORITY, LIGHT_TOKEN_PROGRAM_ID, RENT_SPONSOR, +}; use solana_account::Account; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; @@ -203,13 +202,13 @@ impl RentFreeDecompressAccount { /// /// # Arguments /// * `interface` - The account interface (must be cold/compressed) - /// * `ctoken_variant` - CToken variant (e.g., `CTokenAccountVariant::Vault { cmint }`) + /// * `ctoken_variant` - CToken variant (e.g., `TokenAccountVariant::Vault { cmint }`) /// /// # Example /// ```ignore /// RentFreeDecompressAccount::from_ctoken( /// AccountInterface::cold(vault_pda, compressed_vault.account), - /// CTokenAccountVariant::Vault { cmint: cmint_pda }, + /// TokenAccountVariant::Vault { cmint: cmint_pda }, /// )? /// ``` #[cfg(feature = "anchor")] @@ -240,7 +239,7 @@ pub mod compressible_instruction { use super::*; /// Returns program account metas for decompress_accounts_idempotent with CToken support. - /// Includes ctoken_rent_sponsor, ctoken_program, ctoken_cpi_authority, ctoken_config. + /// Includes ctoken_rent_sponsor, light_token_program, ctoken_cpi_authority, ctoken_config. pub fn accounts( fee_payer: Pubkey, config: Pubkey, @@ -251,8 +250,8 @@ pub mod compressible_instruction { AccountMeta::new_readonly(config, false), AccountMeta::new(rent_sponsor, false), AccountMeta::new(RENT_SPONSOR, false), - AccountMeta::new_readonly(CTOKEN_PROGRAM_ID, false), - AccountMeta::new_readonly(CTOKEN_CPI_AUTHORITY, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(LIGHT_TOKEN_CPI_AUTHORITY, false), AccountMeta::new_readonly(COMPRESSIBLE_CONFIG_V1, false), ] } @@ -418,7 +417,7 @@ pub mod compressible_instruction { // Find the first token account's CPI context let first_token_cpi_context = compressed_accounts .iter() - .find(|(acc, _)| acc.owner == C_TOKEN_PROGRAM_ID.into()) + .find(|(acc, _)| acc.owner == LIGHT_TOKEN_PROGRAM_ID.into()) .map(|(acc, _)| acc.tree_info.cpi_context.unwrap()) .expect("has_tokens is true so there must be a token"); let system_config = diff --git a/sdk-libs/macros/MACRO-NEW.md b/sdk-libs/macros/MACRO-NEW.md index ee00070b3f..cf26f68d81 100644 --- a/sdk-libs/macros/MACRO-NEW.md +++ b/sdk-libs/macros/MACRO-NEW.md @@ -10,9 +10,9 @@ All phases implemented and tested, including Phase 8 CToken seed refactor. See ` ### Phase 8 Key Changes -- `CTokenAccountVariant` now has struct variants with Pubkey fields for seeds -- `PackedCTokenAccountVariant` has struct variants with u8 idx fields -- `CTokenSeedProvider` trait no longer requires accounts struct parameter +- `TokenAccountVariant` now has struct variants with Pubkey fields for seeds +- `PackedTokenAccountVariant` has struct variants with u8 idx fields +- `TokenSeedProvider` trait no longer requires accounts struct parameter - `DecompressAccountsIdempotent` no longer needs named seed accounts - All seed resolution happens via variant idx fields and `post_system_accounts` @@ -197,7 +197,7 @@ pub fn decompress_accounts_idempotent_new( From `csdk-anchor-full-derived-test/tests/basic_test.rs`: ```rust -use csdk_anchor_full_derived_test::{CTokenAccountVariant, CompressedAccountVariant}; +use csdk_anchor_full_derived_test::{TokenAccountVariant, CompressedAccountVariant}; use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ GameSessionSeeds, UserRecordSeeds, // NO SeedParams needed }; @@ -265,8 +265,8 @@ let game_variant = CompressedAccountVariant::game_session( }, ).expect("GameSession seed verification failed"); -let vault_ctoken_data = light_ctoken_sdk::compat::CTokenData { - variant: CTokenAccountVariant::Vault, +let vault_ctoken_data = light_token_sdk::compat::CTokenData { + variant: TokenAccountVariant::Vault, token_data: compressed_vault.token.clone(), }; @@ -524,12 +524,12 @@ cargo test-sbf -p csdk-anchor-full-derived-test ``` Client: - CTokenAccountVariant::Vault // No fields - just an enum tag + TokenAccountVariant::Vault // No fields - just an enum tag + TokenData { owner, mint, amount } → Pack: variant just CLONED (no packing) On-chain: - CTokenSeedProvider::get_seeds(ctx.accounts, remaining_accounts) + TokenSeedProvider::get_seeds(ctx.accounts, remaining_accounts) → ctx.accounts.cmint.as_ref()?.key() // READS FROM NAMED ACCOUNT! → derive PDA with ["vault", cmint] ``` @@ -540,13 +540,13 @@ On-chain: ``` Client: - CTokenAccountVariant::Vault { cmint: Pubkey } // HAS SEED FIELD! + TokenAccountVariant::Vault { cmint: Pubkey } // HAS SEED FIELD! + TokenData { owner, mint, amount } → Pack: variant.cmint → cmint_idx (pushed to remaining_accounts) On-chain: Unpack: cmint_idx → post_system_accounts[cmint_idx].key → cmint Pubkey - CTokenSeedProvider::get_seeds(program_id) + TokenSeedProvider::get_seeds(program_id) → self.cmint // READS FROM VARIANT DIRECTLY! → derive PDA with ["vault", cmint] ``` @@ -557,25 +557,25 @@ On-chain: ```rust // Unpacked (client-side, with Pubkeys) -pub enum CTokenAccountVariant { +pub enum TokenAccountVariant { Vault { cmint: Pubkey }, UserAta { owner: Pubkey, cmint: Pubkey }, // If defined } // Packed (wire format, with indices) -pub enum PackedCTokenAccountVariant { +pub enum PackedTokenAccountVariant { Vault { cmint_idx: u8 }, UserAta { owner_idx: u8, cmint_idx: u8 }, } // Pack impl -impl Pack for CTokenAccountVariant { - type Packed = PackedCTokenAccountVariant; +impl Pack for TokenAccountVariant { + type Packed = PackedTokenAccountVariant; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { match self { - CTokenAccountVariant::Vault { cmint } => { - PackedCTokenAccountVariant::Vault { + TokenAccountVariant::Vault { cmint } => { + PackedTokenAccountVariant::Vault { cmint_idx: remaining_accounts.insert_or_get(*cmint), } } @@ -584,13 +584,13 @@ impl Pack for CTokenAccountVariant { } // Unpack impl -impl Unpack for PackedCTokenAccountVariant { - type Unpacked = CTokenAccountVariant; +impl Unpack for PackedTokenAccountVariant { + type Unpacked = TokenAccountVariant; fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result { match self { - PackedCTokenAccountVariant::Vault { cmint_idx } => { - Ok(CTokenAccountVariant::Vault { + PackedTokenAccountVariant::Vault { cmint_idx } => { + Ok(TokenAccountVariant::Vault { cmint: *remaining_accounts[*cmint_idx as usize].key, }) } @@ -599,11 +599,11 @@ impl Unpack for PackedCTokenAccountVariant { } ``` -### CTokenSeedProvider Trait Change +### TokenSeedProvider Trait Change ```rust // BEFORE (requires accounts struct) -pub trait CTokenSeedProvider: Copy { +pub trait TokenSeedProvider: Copy { type Accounts<'info>; fn get_seeds<'a, 'info>( @@ -614,19 +614,19 @@ pub trait CTokenSeedProvider: Copy { } // AFTER (self-contained) -pub trait CTokenSeedProvider: Copy { +pub trait TokenSeedProvider: Copy { fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; fn get_authority_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; } ``` -### Generated CTokenSeedProvider impl +### Generated TokenSeedProvider impl ```rust -impl CTokenSeedProvider for CTokenAccountVariant { +impl TokenSeedProvider for TokenAccountVariant { fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError> { match self { - CTokenAccountVariant::Vault { cmint } => { + TokenAccountVariant::Vault { cmint } => { // cmint is already resolved Pubkey from variant! let seeds: &[&[u8]] = &[b"vault", cmint.as_ref()]; let (pda, bump) = Pubkey::find_program_address(seeds, program_id); @@ -649,10 +649,10 @@ pub struct DecompressAccountsIdempotent<'info> { pub rent_sponsor: UncheckedAccount<'info>, // CToken static accounts pub ctoken_rent_sponsor: Option>, - pub ctoken_program: Option>, + pub light_token_program: Option>, pub ctoken_cpi_authority: Option>, pub ctoken_config: Option>, - // SEED ACCOUNTS (needed by CTokenSeedProvider) + // SEED ACCOUNTS (needed by TokenSeedProvider) pub authority: Option>, pub mint_authority: Option>, pub user: Option>, @@ -667,7 +667,7 @@ pub struct DecompressAccountsIdempotent<'info> { pub rent_sponsor: UncheckedAccount<'info>, // CToken static accounts pub ctoken_rent_sponsor: Option>, - pub ctoken_program: Option>, + pub light_token_program: Option>, pub ctoken_cpi_authority: Option>, pub ctoken_config: Option>, // NO SEED ACCOUNTS - they're in the variant! @@ -678,7 +678,7 @@ pub struct DecompressAccountsIdempotent<'info> { ```rust // Construct CToken variant with seed pubkeys -let vault_variant = CTokenAccountVariant::Vault { cmint: cmint_pda }; +let vault_variant = TokenAccountVariant::Vault { cmint: cmint_pda }; let ctoken_data = CTokenData { variant: vault_variant, token_data: compressed_vault.token.clone(), @@ -699,7 +699,7 @@ let decompress_instruction = compressible_instruction::decompress_accounts_idemp ```rust /// Returns program account metas for decompress_accounts_idempotent with CToken support. -/// Includes ctoken_rent_sponsor, ctoken_program, ctoken_cpi_authority, ctoken_config. +/// Includes ctoken_rent_sponsor, light_token_program, ctoken_cpi_authority, ctoken_config. pub fn accounts(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey) -> Vec; /// Returns program account metas for PDA-only decompression (no CToken accounts). @@ -711,8 +711,8 @@ pub fn accounts_pda_only(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey | File | Changes | | --------------------------------------------------- | -------------------------------------------------- | | `ctoken-sdk/src/pack.rs` | Add Pack bound to V, use V::Packed for packed type | -| `sdk/src/compressible/decompress_runtime.rs` | Update CTokenSeedProvider trait signature | -| `macros/src/compressible/variant_enum.rs` | Generate CTokenAccountVariant with struct fields | +| `sdk/src/compressible/decompress_runtime.rs` | Update TokenSeedProvider trait signature | +| `macros/src/compressible/variant_enum.rs` | Generate TokenAccountVariant with struct fields | | `macros/src/compressible/seed_providers.rs` | Update get_seeds to use self.field | | `macros/src/compressible/instructions.rs` | Remove seed account fields from Accounts struct | | `ctoken-sdk/src/compressible/decompress_runtime.rs` | Update process_decompress_tokens_runtime | @@ -724,7 +724,7 @@ pub fn accounts_pda_only(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey │ CLIENT SIDE │ ├────────────────────────────────────────────────────────────────────────────┤ │ │ -│ CTokenAccountVariant::Vault { cmint: cmint_pda } │ +│ TokenAccountVariant::Vault { cmint: cmint_pda } │ │ + TokenData { owner, mint, amount } │ │ = CTokenData { variant, token_data } │ │ │ │ @@ -753,7 +753,7 @@ pub fn accounts_pda_only(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey │ = CTokenData { variant: Vault { cmint }, token_data } │ │ │ │ │ ▼ │ -│ CTokenSeedProvider::get_seeds(program_id) │ +│ TokenSeedProvider::get_seeds(program_id) │ │ match self { │ │ Vault { cmint } => seeds = ["vault", cmint.as_ref()] │ │ } │ @@ -769,7 +769,7 @@ pub fn accounts_pda_only(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey ### Implementation Steps 1. **Update SDK trait** (`sdk/src/compressible/decompress_runtime.rs`): - - Change `CTokenSeedProvider` signature to not require `accounts` param + - Change `TokenSeedProvider` signature to not require `accounts` param 2. **Update ctoken-sdk Pack** (`ctoken-sdk/src/pack.rs`): - Add `Pack` trait bound to `V` in `CTokenDataWithVariant` @@ -777,8 +777,8 @@ pub fn accounts_pda_only(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey 3. **Generate CToken variant enums** (`variant_enum.rs`): - Parse token_seeds to extract ctx.\* fields - - Generate `CTokenAccountVariant` with struct variants (Pubkeys) - - Generate `PackedCTokenAccountVariant` with struct variants (indices) + - Generate `TokenAccountVariant` with struct variants (Pubkeys) + - Generate `PackedTokenAccountVariant` with struct variants (indices) - Generate Pack/Unpack impls 4. **Update seed provider generation** (`seed_providers.rs`): @@ -788,5 +788,5 @@ pub fn accounts_pda_only(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey - Remove seed account fields from `DecompressAccountsIdempotent` 6. **Update tests** (`basic_test.rs`): - - Construct `CTokenAccountVariant::Vault { cmint }` with Pubkey + - Construct `TokenAccountVariant::Vault { cmint }` with Pubkey - Remove seed accounts from instruction building diff --git a/sdk-libs/macros/MACRO_REFACTOR.md b/sdk-libs/macros/MACRO_REFACTOR.md index efc17efddb..0627d2c898 100644 --- a/sdk-libs/macros/MACRO_REFACTOR.md +++ b/sdk-libs/macros/MACRO_REFACTOR.md @@ -42,7 +42,7 @@ From global `#[compressible(...)]`: - `DecompressAccountsIdempotent<'info>` with **named** seed accounts - `CompressAccountsIdempotent<'info>` - `PdaSeedDerivation` trait impls -- `CTokenSeedProvider` trait impls +- `TokenSeedProvider` trait impls - Instruction handlers - Client-side seed functions diff --git a/sdk-libs/macros/MACRO_REFACTOR_V2.md b/sdk-libs/macros/MACRO_REFACTOR_V2.md index 091bb27610..8beec9be22 100644 --- a/sdk-libs/macros/MACRO_REFACTOR_V2.md +++ b/sdk-libs/macros/MACRO_REFACTOR_V2.md @@ -92,11 +92,11 @@ Replace the dual-declaration system with a single source of truth: │ │ │ │ │ │ 1. CompressedAccountVariant enum (with struct variants) │ │ │ │ 2. PackedCompressedAccountVariant (with idx fields) │ │ -│ │ 3. CTokenAccountVariant enum │ │ +│ │ 3. TokenAccountVariant enum │ │ │ │ 4. Pack/Unpack impls │ │ │ │ 5. XxxSeeds structs per PDA type │ │ │ │ 6. PdaSeedDerivation trait impls │ │ -│ │ 7. CTokenSeedProvider trait impls │ │ +│ │ 7. TokenSeedProvider trait impls │ │ │ │ 8. DecompressAccountsIdempotent Accounts struct │ │ │ │ 9. decompress_accounts_idempotent() instruction handler │ │ │ │ 10. Client-side seed derivation functions │ │ diff --git a/sdk-libs/macros/OPTION_A_PLAN.md b/sdk-libs/macros/OPTION_A_PLAN.md index 2efa55adb4..7504c14e7d 100644 --- a/sdk-libs/macros/OPTION_A_PLAN.md +++ b/sdk-libs/macros/OPTION_A_PLAN.md @@ -53,7 +53,7 @@ impl Pack for StandardAtaData { fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { let (ata_address, _bump) = - crate::ctoken::get_associated_ctoken_address_and_bump(&self.wallet, &self.mint); + crate::token::get_associated_ctoken_address_and_bump(&self.wallet, &self.mint); // Insert wallet as signer let wallet_index = remaining_accounts.insert_or_get_config(self.wallet, true, false); @@ -101,12 +101,12 @@ let enum_def = quote! { #[derive(Clone, Debug, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] pub enum CompressedAccountVariant { #(#account_variants)* - PackedCTokenData(light_ctoken_sdk::compat::PackedCTokenData), - CTokenData(light_ctoken_sdk::compat::CTokenData), - CompressedMint(light_ctoken_sdk::compat::CompressedMintData), + PackedCTokenData(light_token_sdk::compat::PackedCTokenData), + CTokenData(light_token_sdk::compat::CTokenData), + CompressedMint(light_token_sdk::compat::CompressedMintData), // NEW: Standard ATA variants - StandardAta(light_ctoken_sdk::compat::StandardAtaData), - PackedStandardAta(light_ctoken_sdk::compat::PackedStandardAtaData), + StandardAta(light_token_sdk::compat::StandardAtaData), + PackedStandardAta(light_token_sdk::compat::PackedStandardAtaData), } }; ``` @@ -114,6 +114,7 @@ let enum_def = quote! { ### 2.2 variant_enum.rs - Update trait implementations Add match arms for StandardAta in: + - `DataHasher` impl (unreachable for packed) - `HasCompressionInfo` impl (unreachable - token accounts don't have compression_info) - `Size` impl (unreachable) @@ -163,7 +164,7 @@ Add processing for standard ATAs in `process_decompress_tokens_runtime`: /// Standard ATAs use the same Transfer2 CPI but: /// 1. Don't require program-derived seeds /// 2. Wallet must be a TX signer (validated here) -/// 3. ATA is derived from (wallet, ctoken_program, mint) +/// 3. ATA is derived from (wallet, light_token_program, mint) pub fn process_standard_atas_in_token_flow<'info>( standard_atas: Vec<(PackedStandardAtaData, CompressedAccountMetaNoLamportsNoAddress)>, packed_accounts: &[AccountInfo<'info>], @@ -335,19 +336,19 @@ pub fn decompress_accounts_idempotent( // Pack standard ATAs for ata_input in standard_atas { let (ata_address, _) = derive_ctoken_ata(&ata_input.wallet, &ata_input.mint); - + // Insert wallet as signer remaining_accounts.insert_or_get_config(ata_input.wallet, true, false); remaining_accounts.insert_or_get(ata_input.mint); remaining_accounts.insert_or_get(ata_address); - + let standard_ata = StandardAtaData { wallet: ata_input.wallet, mint: ata_input.mint, token_data: ata_input.token_data.clone(), }; let packed = standard_ata.pack(&mut remaining_accounts); - + typed_compressed_accounts.push(CompressedAccountData { meta: /* from validity_proof_with_context */, data: CompressedAccountVariant::PackedStandardAta(packed), @@ -362,6 +363,7 @@ for ata_input in standard_atas { ### 6.1 Update existing test Modify `test_create_pdas_and_mint_auto` to: + 1. Use `StandardAtaInput` for user ATA decompression 2. Verify wallet signer requirement 3. Test mixed batch (PDAs + program tokens + standard ATAs) @@ -395,7 +397,7 @@ Modify `test_create_pdas_and_mint_auto` to: **A: Separate variant (`PackedStandardAta`) for cleaner handling and explicit wallet index.** 2. **Q: How does the client know which accounts are standard ATAs vs program tokens?** - **A: Client explicitly creates `StandardAtaInput` vs wrapping in program's `CTokenAccountVariant`.** + **A: Client explicitly creates `StandardAtaInput` vs wrapping in program's `TokenAccountVariant`.** 3. **Q: Do standard ATAs require `cmint_authority` account?** **A: No. Standard ATAs only need wallet signer. `cmint_authority` is only for mint decompression.** diff --git a/sdk-libs/macros/OPTION_A_STATE_FLOW.md b/sdk-libs/macros/OPTION_A_STATE_FLOW.md index 0a0d62d1ec..5b6e879a05 100644 --- a/sdk-libs/macros/OPTION_A_STATE_FLOW.md +++ b/sdk-libs/macros/OPTION_A_STATE_FLOW.md @@ -39,11 +39,11 @@ Option A adds `StandardAta` and `PackedStandardAta` variants to the macro-genera +-------------+ +-------------+ +-------------+ | | | v v v - derive seeds derive_ctoken_ata find_cmint_address + derive seeds derive_ctoken_ata find_mint_address from variant (wallet, mint) (mint_seed) | | | v v v - CreateCToken CreateAssociated DecompressCMint + CreateCToken CreateAssociated DecompressMint AccountCpi CTokenAccountCpi Cpi (invoke_signed) (invoke - wallet (invoke) signs tx) @@ -76,7 +76,7 @@ Option A adds `StandardAta` and `PackedStandardAta` variants to the macro-genera wallet must be TX signer program signs via CPI | | v v - CreateAssociatedCTokenAccountCpi CreateCTokenAccountCpi + CreateAssociatedCTokenAccountCpi CreateTokenAccountCpi .invoke() - no program signer .invoke_signed(&[seeds]) ``` @@ -185,14 +185,14 @@ pub enum CompressedAccountVariant { PackedUserRecord(PackedUserRecord), GameSession(GameSession), PackedGameSession(PackedGameSession), - + // === Token variants (macro-generated) === - PackedCTokenData(PackedCTokenData), - CTokenData(CTokenData), - + PackedCTokenData(PackedCTokenData), + CTokenData(CTokenData), + // === Mint variant (existing) === CompressedMint(CompressedMintData), - + // === NEW: Standard ATA variants (always present) === StandardAta(StandardAtaData), PackedStandardAta(PackedStandardAtaData), @@ -209,27 +209,27 @@ pub enum CompressedAccountVariant { Case 1: Single Type (no CPI context) ------------------------------------ PDAs only: LightSystemProgramCpi.invoke() - Mints only: DecompressCMintCpi.invoke() + Mints only: DecompressMintCpi.invoke() Tokens only: Transfer2 CPI invoke() Case 2: Multi-Type (with CPI context batching) ---------------------------------------------- - + PDAs Mints Tokens CPI Context Action ---- ----- ------ ------------------ Yes No No execute directly (no context) - No Yes No execute directly (no context) + No Yes No execute directly (no context) No No Yes execute directly (no context) - + Yes Yes No PDAs: first_set_context Mints: execute (consume) - + Yes No Yes PDAs: first_set_context Tokens: execute (consume) - + No Yes Yes Mints: first_set_context Tokens: execute (consume) - + Yes Yes Yes PDAs: first_set_context Mints: set_context Tokens: execute (consume) @@ -247,7 +247,7 @@ pub enum CompressedAccountVariant { ### CompressedMint Validation -1. **CMint derivation**: `find_cmint_address(mint_seed) == cmint_pda` +1. **CMint derivation**: `find_mint_address(mint_seed) == cmint_pda` 2. **Authority**: fee_payer must be mint authority OR explicit cmint_authority provided ### Program Token (Vault) Validation diff --git a/sdk-libs/macros/OVERVIEW.md b/sdk-libs/macros/OVERVIEW.md index ef762598ec..9aadb63e2e 100644 --- a/sdk-libs/macros/OVERVIEW.md +++ b/sdk-libs/macros/OVERVIEW.md @@ -58,11 +58,11 @@ pub const MY_BYTES: &[u8] = b"my_bytes"; // &[u8] constant The macro generates: 1. **`CompressedAccountVariant`** - enum with all PDA types + token variants -2. **`CTokenAccountVariant`** - enum for token account types +2. **`TokenAccountVariant`** - enum for token account types 3. **`DecompressAccountsIdempotent`** - Anchor accounts struct 4. **`CompressAccountsIdempotent`** - Anchor accounts struct 5. **`SeedParams`** - struct for `data.*` seed fields -6. **`CTokenSeedProvider`** impl - derives token seeds +6. **`TokenSeedProvider`** impl - derives token seeds 7. **`PdaSeedDerivation`** impl - derives PDA seeds 8. **`DecompressContext`** impl - runtime decompression logic 9. **`decompress_accounts_idempotent()`** - instruction handler @@ -135,7 +135,7 @@ When decompressing **both PDAs and tokens** in one instruction: // Uses first TOKEN's tree context since tokens execute last let first_token_cpi_context = compressed_accounts .iter() - .find(|(acc, _)| acc.owner == C_TOKEN_PROGRAM_ID) + .find(|(acc, _)| acc.owner == LIGHT_TOKEN_PROGRAM_ID) .map(|(acc, _)| acc.tree_info.cpi_context.unwrap()); ``` diff --git a/sdk-libs/macros/SPEC_OPTION_A.md b/sdk-libs/macros/SPEC_OPTION_A.md index 56309fb4d0..a8fe485677 100644 --- a/sdk-libs/macros/SPEC_OPTION_A.md +++ b/sdk-libs/macros/SPEC_OPTION_A.md @@ -2,7 +2,7 @@ ## Overview -Add `StandardAta` and `StandardMint` as always-present variants in the macro-generated `CTokenAccountVariant` enum. Programs automatically get these standard variants without declaration. +Add `StandardAta` and `StandardMint` as always-present variants in the macro-generated `TokenAccountVariant` enum. Programs automatically get these standard variants without declaration. ## Goals @@ -23,7 +23,7 @@ Add `StandardAta` and `StandardMint` as always-present variants in the macro-gen #[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] pub struct StandardAtaData { /// Wallet owner pubkey - MUST be a signer on the transaction. - /// The ATA is derived from (wallet, ctoken_program_id, mint). + /// The ATA is derived from (wallet, light_token_program_id, mint). pub wallet: Pubkey, /// Mint pubkey for this token account. pub mint: Pubkey, @@ -59,7 +59,7 @@ pub type StandardMintData = CompressedMintData; #[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] pub struct CompressedMintData { - /// Mint seed pubkey (used to derive CMint PDA via find_cmint_address). + /// Mint seed pubkey (used to derive CMint PDA via find_mint_address). pub mint_seed_pubkey: Pubkey, /// Compressed mint with context (from indexer). pub compressed_mint_with_context: CompressedMintWithContext, @@ -74,7 +74,7 @@ pub struct CompressedMintData { ## Enum Changes -### CTokenAccountVariant (Modified) +### TokenAccountVariant (Modified) The macro will always generate these standard variants: @@ -82,17 +82,17 @@ The macro will always generate these standard variants: // sdk-libs/macros/src/compressible/seed_providers.rs pub fn generate_ctoken_account_variant_enum(specs: &[TokenSeedSpec]) -> Result { // ... existing program-specific variants ... - + quote! { #[derive(Clone, Copy, Debug, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] - pub enum CTokenAccountVariant { + pub enum TokenAccountVariant { // Program-specific variants (from macro args) #(#program_variants,)* - + // Standard variants (always present) - /// Standard ATA - uses fixed derivation (wallet, ctoken_program, mint). + /// Standard ATA - uses fixed derivation (wallet, light_token_program, mint). StandardAta, - /// Standard Mint - uses fixed derivation find_cmint_address(mint_seed). + /// Standard Mint - uses fixed derivation find_mint_address(mint_seed). StandardMint, } } @@ -106,14 +106,14 @@ pub fn generate_ctoken_account_variant_enum(specs: &[TokenSeedSpec]) -> Result), - CTokenData(CTokenData), - + PackedCTokenData(PackedCTokenData), + CTokenData(CTokenData), + // Mint variant (existing) CompressedMint(CompressedMintData), - + // NEW: Standard ATA variant (separate from CTokenData for cleaner handling) StandardAta(StandardAtaData), PackedStandardAta(PackedStandardAtaData), @@ -124,12 +124,12 @@ pub enum CompressedAccountVariant { ## Trait Implementations -### CTokenSeedProvider for StandardAta +### TokenSeedProvider for StandardAta ```rust -impl CTokenSeedProvider for CTokenAccountVariant { +impl TokenSeedProvider for TokenAccountVariant { // ... existing match arms ... - + fn get_seeds<'a, 'info>( &self, accounts: &'a Self::Accounts<'info>, @@ -137,33 +137,33 @@ impl CTokenSeedProvider for CTokenAccountVariant { ) -> Result<(Vec>, Pubkey), ProgramError> { match self { // ... existing program-specific arms ... - - CTokenAccountVariant::StandardAta => { + + TokenAccountVariant::StandardAta => { // StandardAta doesn't use program seeds - derivation is fixed. // Return empty seeds; the runtime handles ATA creation separately. Err(ProgramError::InvalidArgument) // Should not be called } - CTokenAccountVariant::StandardMint => { + TokenAccountVariant::StandardMint => { // StandardMint doesn't use program seeds - derivation is fixed. Err(ProgramError::InvalidArgument) // Should not be called } } } - + fn get_authority_seeds<'a, 'info>(...) -> Result<...> { match self { - CTokenAccountVariant::StandardAta => { + TokenAccountVariant::StandardAta => { Err(ProgramError::InvalidArgument) // ATAs don't need authority seeds } - CTokenAccountVariant::StandardMint => { + TokenAccountVariant::StandardMint => { Err(ProgramError::InvalidArgument) // Mints don't need authority seeds for decompress } // ... existing arms ... } } - + fn is_ata(&self) -> bool { - matches!(self, CTokenAccountVariant::StandardAta) + matches!(self, TokenAccountVariant::StandardAta) } } ``` @@ -175,19 +175,19 @@ impl HasTokenVariant for CompressedAccountData { fn is_packed_ctoken(&self) -> bool { matches!( self.data, - CompressedAccountVariant::PackedCTokenData(_) + CompressedAccountVariant::PackedCTokenData(_) | CompressedAccountVariant::PackedStandardAta(_) ) } - + fn is_compressed_mint(&self) -> bool { matches!(self.data, CompressedAccountVariant::CompressedMint(_)) } - + fn is_standard_ata(&self) -> bool { matches!( self.data, - CompressedAccountVariant::StandardAta(_) + CompressedAccountVariant::StandardAta(_) | CompressedAccountVariant::PackedStandardAta(_) ) } @@ -199,19 +199,19 @@ impl HasTokenVariant for CompressedAccountData { ```rust impl Pack for StandardAtaData { type Packed = PackedStandardAtaData; - + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { // Derive ATA address from wallet + mint let (ata_address, _bump) = derive_ctoken_ata(&self.wallet, &self.mint); - + // Insert all required accounts let wallet_index = remaining_accounts.insert_or_get_config(self.wallet, true, false); // signer let mint_index = remaining_accounts.insert_or_get(self.mint); let ata_index = remaining_accounts.insert_or_get(ata_address); - + // Pack token data let token_data = self.token_data.pack(remaining_accounts); - + PackedStandardAtaData { wallet_index, mint_index, @@ -223,7 +223,7 @@ impl Pack for StandardAtaData { impl Unpack for PackedStandardAtaData { type Unpacked = StandardAtaData; - + fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result { let wallet = *remaining_accounts .get(self.wallet_index as usize) @@ -234,7 +234,7 @@ impl Unpack for PackedStandardAtaData { .ok_or(ProgramError::NotEnoughAccountKeys)? .key; let token_data = self.token_data.unpack(remaining_accounts)?; - + Ok(StandardAtaData { wallet, mint, token_data }) } } @@ -255,26 +255,26 @@ pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( standard_atas: Vec<(PackedStandardAtaData, CompressedAccountMetaNoLamportsNoAddress)>, ) -> Result<(), ProgramError> { // ... existing token processing ... - + // Process standard ATAs for (packed_ata, meta) in standard_atas.into_iter() { let wallet_info = &packed_accounts[packed_ata.wallet_index as usize]; let mint_info = &packed_accounts[packed_ata.mint_index as usize]; let ata_info = &packed_accounts[packed_ata.ata_index as usize]; - + // Verify wallet is signer if !wallet_info.is_signer { msg!("StandardAta wallet must be signer: {:?}", wallet_info.key); return Err(ProgramError::MissingRequiredSignature); } - + // Verify ATA derivation let (derived_ata, bump) = derive_ctoken_ata(wallet_info.key, mint_info.key); if derived_ata != *ata_info.key { msg!("ATA derivation mismatch: derived={:?}, provided={:?}", derived_ata, ata_info.key); return Err(ProgramError::InvalidAccountData); } - + // Create ATA if needed (idempotent) CreateAssociatedCTokenAccountCpi { payer: fee_payer.clone(), @@ -295,11 +295,11 @@ pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( }, idempotent: true, }.invoke()?; - + // Build decompress indices let owner_index = packed_ata.token_data.owner; // ATA address index let wallet_account_index = packed_ata.wallet_index; - + let source = MultiInputTokenDataWithContext { owner: owner_index, amount: packed_ata.token_data.amount, @@ -310,7 +310,7 @@ pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( merkle_context: meta.tree_info.into(), root_index: meta.tree_info.root_index, }; - + let tlv = vec![ExtensionInstructionData::CompressedOnly( CompressedOnlyExtensionInstructionData { delegated_amount: 0, @@ -322,7 +322,7 @@ pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( owner_index: wallet_account_index, }, )]; - + let decompress_index = DecompressFullIndices { source, destination_index: packed_ata.ata_index, @@ -331,7 +331,7 @@ pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( }; token_decompress_indices.push(decompress_index); } - + // ... rest of existing logic (single Transfer2 CPI) ... } ``` @@ -352,21 +352,21 @@ fn collect_all_accounts<'a, 'b, 'info>( Vec<(PackedStandardAtaData, Meta)>, // NEW: Standard ATAs ), ProgramError> { // ... existing setup ... - + let mut standard_ata_accounts = Vec::new(); - + for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { let meta = compressed_data.meta; match compressed_data.data { // ... existing PDA arms ... - + CompressedAccountVariant::PackedCTokenData(data) => { compressed_token_accounts.push((data, meta)); } CompressedAccountVariant::CompressedMint(data) => { compressed_mint_accounts.push((data, meta)); } - + // NEW: Standard ATA handling CompressedAccountVariant::PackedStandardAta(data) => { standard_ata_accounts.push((data, meta)); @@ -374,11 +374,11 @@ fn collect_all_accounts<'a, 'b, 'info>( CompressedAccountVariant::StandardAta(_) => { unreachable!("Unpacked StandardAta should not appear"); } - + // ... other arms ... } } - + Ok((compressed_pda_infos, compressed_token_accounts, compressed_mint_accounts, standard_ata_accounts)) } ``` @@ -399,7 +399,7 @@ pub fn decompress_accounts_idempotent( compressed_accounts: &[(CompressedAccount, T)], // NEW: Standard ATAs standard_atas: &[StandardAtaInput], - // NEW: Standard Mints + // NEW: Standard Mints standard_mints: &[StandardMintInput], program_account_metas: &[AccountMeta], validity_proof_with_context: ValidityProofWithContext, @@ -408,14 +408,14 @@ where T: Pack + Clone + std::fmt::Debug, { // ... existing setup ... - + // Pack standard ATAs for ata_input in standard_atas { let (ata_address, _) = derive_ctoken_ata(&ata_input.wallet, &ata_input.mint); remaining_accounts.insert_or_get_config(ata_input.wallet, true, false); // signer remaining_accounts.insert_or_get(ata_input.mint); remaining_accounts.insert_or_get(ata_address); - + // Build StandardAtaData and pack let standard_ata = StandardAtaData { wallet: ata_input.wallet, @@ -423,19 +423,19 @@ where token_data: ata_input.token_data.clone(), }; let packed = standard_ata.pack(&mut remaining_accounts); - + typed_compressed_accounts.push(CompressedAccountData { meta: /* from validity_proof_with_context */, data: CompressedAccountVariant::PackedStandardAta(packed), }); } - + // Pack standard mints for mint_input in standard_mints { - let (cmint_address, _) = find_cmint_address(&mint_input.mint_seed); + let (cmint_address, _) = find_mint_address(&mint_input.mint_seed); remaining_accounts.insert_or_get(mint_input.mint_seed); remaining_accounts.insert_or_get(cmint_address); - + typed_compressed_accounts.push(CompressedAccountData { meta: /* from validity_proof_with_context */, data: CompressedAccountVariant::CompressedMint(CompressedMintData { @@ -446,7 +446,7 @@ where }), }); } - + // ... rest of instruction building ... } @@ -479,28 +479,28 @@ pub struct StandardMintInput { pub struct DecompressAccountsIdempotent<'info> { #[account(mut)] pub fee_payer: Signer<'info>, - + /// Program's compressible config pub config: AccountInfo<'info>, - + /// Program's rent sponsor #[account(mut)] pub rent_sponsor: AccountInfo<'info>, - + // CToken accounts - REQUIRED if any tokens/ATAs/mints present /// CToken rent sponsor (ctoken program's rent sponsor PDA) #[account(mut)] pub ctoken_rent_sponsor: Option>, - + /// CToken compressible config pub ctoken_config: Option>, - + /// CToken program - pub ctoken_program: Option>, - + pub light_token_program: Option>, + /// CToken CPI authority pub ctoken_cpi_authority: Option>, - + // ... other optional accounts for program-specific seeds ... } ``` @@ -517,11 +517,11 @@ The runtime will validate that ctoken accounts are Some when standard ATAs/mints - `token_data.owner` (ATA address) must match derived ATA 2. **Standard Mint validation:** - - `find_cmint_address(mint_seed)` must equal the CMint destination account + - `find_mint_address(mint_seed)` must equal the CMint destination account - No signature required (mint authority doesn't need to sign for decompress) 3. **Account requirements:** - - If any standard ATAs or mints present: ctoken_config, ctoken_rent_sponsor, ctoken_program, ctoken_cpi_authority must be Some + - If any standard ATAs or mints present: ctoken_config, ctoken_rent_sponsor, light_token_program, ctoken_cpi_authority must be Some - If only PDAs: ctoken accounts can be None --- @@ -544,4 +544,3 @@ The runtime will validate that ctoken accounts are Some when standard ATAs/mints 1. Existing programs: No changes required, StandardAta/StandardMint variants available automatically 2. New programs: Can use standard types without declaring in macro 3. Tests: Update to use new client helper signature - diff --git a/sdk-libs/macros/SPEC_OPTION_B.md b/sdk-libs/macros/SPEC_OPTION_B.md index e9f52ff165..0ea38914c8 100644 --- a/sdk-libs/macros/SPEC_OPTION_B.md +++ b/sdk-libs/macros/SPEC_OPTION_B.md @@ -33,16 +33,16 @@ pub struct DecompressMultipleAccountsIdempotentData { pub struct DecompressAccountsIdempotentData { /// Validity proof covering ALL accounts (PDAs + ATAs + Mints). pub proof: ValidityProof, - + /// Program-specific compressed accounts (PDAs and program-owned tokens). pub compressed_accounts: Vec>, - + /// Standard ATAs - fixed derivation, wallet signs. pub standard_atas: Vec, - + /// Standard Mints - fixed derivation, no signature required. pub standard_mints: Vec, - + /// Offset to system accounts in remaining_accounts. pub system_accounts_offset: u8, } @@ -113,7 +113,7 @@ pub struct PackedTokenData { /// Standard Mint data for client-side instruction building. #[derive(Clone, Debug)] pub struct StandardMintData { - /// Mint seed pubkey (derives CMint via find_cmint_address). + /// Mint seed pubkey (derives CMint via find_mint_address). pub mint_seed: Pubkey, /// Compressed mint with context from indexer. pub compressed_mint_with_context: CompressedMintWithContext, @@ -177,7 +177,7 @@ where let has_program_accounts = !compressed_accounts.is_empty(); let has_standard_atas = !standard_atas.is_empty(); let has_standard_mints = !standard_mints.is_empty(); - + // Check ctoken accounts required if (has_standard_atas || has_standard_mints) { // Validate ctoken accounts are present @@ -190,32 +190,32 @@ where ProgramError::NotEnoughAccountKeys })?; } - + // Count types for CPI context batching let (has_tokens, has_pdas, has_mints) = check_account_types(&compressed_accounts); let has_any_tokens = has_tokens || has_standard_atas; let has_any_mints = has_mints || has_standard_mints; - + let type_count = has_any_tokens as u8 + has_pdas as u8 + has_any_mints as u8; let needs_cpi_context = type_count >= 2; - + // ... setup CPI accounts ... - + // 1. Process PDAs (if any) - from compressed_accounts - let (compressed_pda_infos, compressed_token_accounts, program_mint_accounts) = + let (compressed_pda_infos, compressed_token_accounts, program_mint_accounts) = ctx.collect_all_accounts(...)?; - + if !compressed_pda_infos.is_empty() { // ... existing PDA processing with CPI context ... } - + // 2. Process Mints (standard + program-specific) let all_mints: Vec<_> = standard_mints .into_iter() .map(|m| (m.into_compressed_mint_data(), m.meta)) .chain(program_mint_accounts) .collect(); - + if !all_mints.is_empty() { process_all_mints( ctx, @@ -226,7 +226,7 @@ where has_any_tokens, // has_subsequent )?; } - + // 3. Process Tokens (standard ATAs + program-specific) if has_any_tokens { process_all_tokens( @@ -240,7 +240,7 @@ where program_id, )?; } - + Ok(()) } ``` @@ -273,13 +273,13 @@ pub fn process_standard_atas<'info>( let ata_info = packed_accounts .get(packed_ata.ata_destination_index as usize) .ok_or(ProgramError::NotEnoughAccountKeys)?; - + // CRITICAL: Verify wallet is signer if !wallet_info.is_signer { msg!("StandardAta: wallet must be signer: {:?}", wallet_info.key); return Err(ProgramError::MissingRequiredSignature); } - + // Derive and verify ATA address let (derived_ata, bump) = derive_ctoken_ata(wallet_info.key, mint_info.key); if derived_ata != *ata_info.key { @@ -289,7 +289,7 @@ pub fn process_standard_atas<'info>( ); return Err(ProgramError::InvalidAccountData); } - + // Verify token_data.owner matches ATA address let owner_info = packed_accounts .get(packed_ata.token_data.owner_index as usize) @@ -301,7 +301,7 @@ pub fn process_standard_atas<'info>( ); return Err(ProgramError::InvalidAccountData); } - + // Create ATA (idempotent) CreateAssociatedCTokenAccountCpi { payer: fee_payer.clone(), @@ -322,7 +322,7 @@ pub fn process_standard_atas<'info>( }, idempotent: true, }.invoke()?; - + // Build decompress indices let source = MultiInputTokenDataWithContext { owner: packed_ata.token_data.owner_index, @@ -334,7 +334,7 @@ pub fn process_standard_atas<'info>( merkle_context: packed_ata.meta.tree_info.into(), root_index: packed_ata.meta.tree_info.root_index, }; - + // Build TLV for ATA let tlv = vec![ExtensionInstructionData::CompressedOnly( CompressedOnlyExtensionInstructionData { @@ -347,7 +347,7 @@ pub fn process_standard_atas<'info>( owner_index: packed_ata.wallet_index, }, )]; - + decompress_indices.push(DecompressFullIndices { source, destination_index: packed_ata.ata_destination_index, @@ -355,7 +355,7 @@ pub fn process_standard_atas<'info>( is_ata: true, }); } - + Ok(()) } ``` @@ -382,17 +382,17 @@ pub fn process_standard_mints<'info>( if standard_mints.is_empty() { return Ok(()); } - + let mint_count = standard_mints.len(); let last_idx = mint_count - 1; - + let mints_only = !has_prior_context && !has_subsequent; let cpi_context_account = if mints_only { None } else { Some(cpi_accounts.cpi_context()?.clone()) }; - + // Build system accounts once let system_accounts = SystemAccountInfos { light_system_program: cpi_accounts.get_account_info(0)?.clone(), @@ -402,11 +402,11 @@ pub fn process_standard_mints<'info>( account_compression_program: cpi_accounts.account_compression_program()?.clone(), system_program: cpi_accounts.system_program()?.clone(), }; - + let state_tree = cpi_accounts.get_tree_account_info(0)?; let input_queue = cpi_accounts.get_tree_account_info(1)?; let output_queue = cpi_accounts.get_tree_account_info(2)?; - + for (idx, packed_mint) in standard_mints.into_iter().enumerate() { // Get accounts from indices let mint_seed_info = packed_accounts @@ -415,9 +415,9 @@ pub fn process_standard_mints<'info>( let cmint_info = packed_accounts .get(packed_mint.cmint_destination_index as usize) .ok_or(ProgramError::NotEnoughAccountKeys)?; - + // Verify CMint derivation - let (derived_cmint, _) = find_cmint_address(mint_seed_info.key); + let (derived_cmint, _) = find_mint_address(mint_seed_info.key); if derived_cmint != *cmint_info.key { msg!( "StandardMint: derivation mismatch. mint_seed={:?}, expected={:?}, got={:?}", @@ -425,10 +425,10 @@ pub fn process_standard_mints<'info>( ); return Err(ProgramError::InvalidAccountData); } - + if mints_only { // Direct execution - DecompressCMintCpi { + DecompressMintCpi { mint_seed: mint_seed_info.clone(), authority: fee_payer.clone(), // No authority check for decompress payer: fee_payer.clone(), @@ -449,7 +449,7 @@ pub fn process_standard_mints<'info>( let is_first = !has_prior_context && idx == 0; let is_last = idx == last_idx; let should_execute = is_last && !has_subsequent; - + let cpi_ctx = if should_execute { CpiContext { first_set_context: false, set_context: false, ..Default::default() } } else if is_first { @@ -457,7 +457,7 @@ pub fn process_standard_mints<'info>( } else { CpiContext { first_set_context: false, set_context: true, ..Default::default() } }; - + DecompressCMintCpiWithContext { mint_seed: mint_seed_info.clone(), authority: fee_payer.clone(), @@ -479,7 +479,7 @@ pub fn process_standard_mints<'info>( }.invoke()?; } } - + Ok(()) } ``` @@ -512,15 +512,15 @@ where T: Pack + Clone + std::fmt::Debug, { let mut remaining_accounts = PackedAccounts::default(); - + // Determine if we need CPI context let has_pdas = !compressed_accounts.is_empty(); - let has_tokens_or_atas = compressed_accounts.iter().any(|(ca, _)| ca.owner == C_TOKEN_PROGRAM_ID.into()) + let has_tokens_or_atas = compressed_accounts.iter().any(|(ca, _)| ca.owner == LIGHT_TOKEN_PROGRAM_ID.into()) || !standard_atas.is_empty(); let has_mints = !standard_mints.is_empty(); - + let needs_cpi_context = (has_pdas as u8 + has_tokens_or_atas as u8 + has_mints as u8) >= 2; - + // Setup system accounts if needs_cpi_context { let cpi_context = compressed_accounts.first() @@ -528,7 +528,7 @@ where .or_else(|| standard_mints.first().map(|_| /* get from proof */)) .ok_or("No accounts to process")? .0.tree_info.cpi_context.unwrap(); - + remaining_accounts.add_system_accounts_v2( SystemAccountMetaConfig::new_with_cpi_context(*program_id, cpi_context) )?; @@ -537,49 +537,49 @@ where SystemAccountMetaConfig::new(*program_id) )?; } - + // Pack output queue let output_queue = get_output_queue(&validity_proof_with_context.accounts[0].tree_info); let output_state_tree_index = remaining_accounts.insert_or_get(output_queue); - + // Pack tree infos let packed_tree_infos = validity_proof_with_context.pack_tree_infos(&mut remaining_accounts); - + // 1. Pack program-specific compressed accounts let mut typed_compressed_accounts = Vec::new(); for (i, (compressed_account, data)) in compressed_accounts.iter().enumerate() { remaining_accounts.insert_or_get(compressed_account.tree_info.queue); let tree_info = packed_tree_infos.state_trees.as_ref().unwrap().packed_tree_infos[i]; let packed_data = data.pack(&mut remaining_accounts); - + typed_compressed_accounts.push(CompressedAccountData { meta: CompressedAccountMetaNoLamportsNoAddress { tree_info, output_state_tree_index }, data: packed_data, }); } - + // 2. Pack standard ATAs let mut packed_standard_atas = Vec::new(); for ata in standard_atas { // Derive ATA address let (ata_address, _) = derive_ctoken_ata(&ata.wallet, &ata.mint); - + // Insert accounts (wallet as signer) let wallet_index = remaining_accounts.insert_or_get_config(ata.wallet, true, false); let mint_index = remaining_accounts.insert_or_get(ata.mint); let ata_destination_index = remaining_accounts.insert_or_get(ata_address); - + // Pack token data // CRITICAL: token_data.owner = ATA address (from compressed account) let owner_index = remaining_accounts.insert_or_get(ata.token_data.owner); // ATA address let delegate_index = ata.token_data.delegate .map(|d| remaining_accounts.insert_or_get(d)) .unwrap_or(0); - + // Get tree info for this account from validity proof let tree_info_idx = compressed_accounts.len() + packed_standard_atas.len(); let tree_info = packed_tree_infos.state_trees.as_ref().unwrap().packed_tree_infos[tree_info_idx]; - + packed_standard_atas.push(PackedStandardAtaData { wallet_index, mint_index, @@ -595,18 +595,18 @@ where meta: CompressedAccountMetaNoLamportsNoAddress { tree_info, output_state_tree_index }, }); } - + // 3. Pack standard mints let mut packed_standard_mints = Vec::new(); for mint in standard_mints { - let (cmint_address, _) = find_cmint_address(&mint.mint_seed); - + let (cmint_address, _) = find_mint_address(&mint.mint_seed); + let mint_seed_index = remaining_accounts.insert_or_get(mint.mint_seed); let cmint_destination_index = remaining_accounts.insert_or_get(cmint_address); - + let tree_info_idx = compressed_accounts.len() + packed_standard_atas.len() + packed_standard_mints.len(); let tree_info = packed_tree_infos.state_trees.as_ref().unwrap().packed_tree_infos[tree_info_idx]; - + packed_standard_mints.push(PackedStandardMintData { mint_seed_index, cmint_destination_index, @@ -616,29 +616,29 @@ where meta: CompressedAccountMetaNoLamportsNoAddress { tree_info, output_state_tree_index }, }); } - + // Build accounts let mut accounts = program_account_metas.to_vec(); let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); accounts.extend(system_accounts); - + // Add PDA destination accounts for pda in decompressed_pda_addresses { accounts.push(AccountMeta::new(*pda, false)); } - + // Add ATA destination accounts for ata in standard_atas { let (ata_address, _) = derive_ctoken_ata(&ata.wallet, &ata.mint); accounts.push(AccountMeta::new(ata_address, false)); } - + // Add CMint destination accounts for mint in standard_mints { - let (cmint_address, _) = find_cmint_address(&mint.mint_seed); + let (cmint_address, _) = find_mint_address(&mint.mint_seed); accounts.push(AccountMeta::new(cmint_address, false)); } - + // Serialize instruction data let instruction_data = DecompressAccountsIdempotentData { proof: validity_proof_with_context.proof, @@ -647,12 +647,12 @@ where standard_mints: packed_standard_mints, system_accounts_offset: system_accounts_offset as u8, }; - + let serialized = instruction_data.try_to_vec()?; let mut data = Vec::with_capacity(discriminator.len() + serialized.len()); data.extend_from_slice(discriminator); data.extend_from_slice(&serialized); - + Ok(Instruction { program_id: *program_id, accounts, @@ -743,14 +743,14 @@ pub struct DecompressAccountsIdempotent<'info> { pub config: AccountInfo<'info>, #[account(mut)] pub rent_sponsor: AccountInfo<'info>, - + // Required when standard ATAs or Mints present #[account(mut)] pub ctoken_rent_sponsor: Option>, pub ctoken_config: Option>, - pub ctoken_program: Option>, + pub light_token_program: Option>, pub ctoken_cpi_authority: Option>, - + // ... program-specific optional accounts ... } ``` @@ -767,7 +767,7 @@ Same as Option A: - `token_data.owner == ata_destination` (ATA address) 2. **Standard Mint validation:** - - `find_cmint_address(mint_seed) == cmint_destination` + - `find_mint_address(mint_seed) == cmint_destination` - No signature required 3. **Account requirements:** @@ -794,4 +794,3 @@ Same as Option A: 1. All existing callers must update to new instruction format 2. Tests need to pass empty vecs for standard_atas/standard_mints if not using 3. No backward compatibility - clean break - diff --git a/sdk-libs/macros/src/compressible/README.md b/sdk-libs/macros/src/compressible/README.md index 7957f520d8..aac0ceb9e3 100644 --- a/sdk-libs/macros/src/compressible/README.md +++ b/sdk-libs/macros/src/compressible/README.md @@ -25,7 +25,7 @@ Procedural macros for generating rent-free account types and their hooks for Sol **`instructions.rs`** - Instruction generation -- Main macro: `#[compressible]` +- Main macro: `#[rentfree]` - Generates compress/decompress instruction handlers - Creates context structs and account validation - **Compress**: PDA-only (ctokens compressed via registry) diff --git a/sdk-libs/macros/src/compressible/anchor_seeds.rs b/sdk-libs/macros/src/compressible/anchor_seeds.rs index cfee10eb00..5c32c820e8 100644 --- a/sdk-libs/macros/src/compressible/anchor_seeds.rs +++ b/sdk-libs/macros/src/compressible/anchor_seeds.rs @@ -5,7 +5,7 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{parse::Parse, Expr, Ident, ItemStruct, Type}; +use syn::{Expr, Ident, ItemStruct, Type}; /// Classified seed element from Anchor's seeds array #[derive(Clone, Debug)] @@ -35,6 +35,8 @@ pub enum ClassifiedSeed { pub struct ExtractedSeedSpec { /// The field name in the Accounts struct pub field_name: Ident, + /// The variant name derived from field_name (snake_case -> CamelCase) + pub variant_name: Ident, /// The inner type (e.g., UserRecord from Account<'info, UserRecord>) pub inner_type: Ident, /// Whether it's Box> @@ -113,19 +115,32 @@ pub fn extract_from_accounts_struct( // Extract seeds from #[account(seeds = [...])] let seeds = extract_anchor_seeds(&field.attrs)?; + // Derive variant name from field name: snake_case -> CamelCase + let variant_name = { + let camel = snake_to_camel_case(&field_ident.to_string()); + Ident::new(&camel, field_ident.span()) + }; + pda_fields.push(ExtractedSeedSpec { field_name: field_ident, + variant_name, inner_type, is_boxed, seeds, }); } else if let Some(token_attr) = token_attr { - // Token field with explicit variant mapping + // Token field - derive variant name from field name if not provided let seeds = extract_anchor_seeds(&field.attrs)?; + // Derive variant name: snake_case field -> CamelCase variant + let variant_name = token_attr.variant_name.unwrap_or_else(|| { + let camel = snake_to_camel_case(&field_ident.to_string()); + Ident::new(&camel, field_ident.span()) + }); + token_fields.push(ExtractedTokenSpec { field_name: field_ident, - variant_name: token_attr.variant_name, + variant_name, seeds, authority_field: None, // Use authority from attribute if provided @@ -183,75 +198,114 @@ pub fn extract_from_accounts_struct( /// Parsed #[rentfree_token(...)] attribute struct RentFreeTokenAttr { - variant_name: Ident, + /// Optional variant name - if None, derived from field name + variant_name: Option, authority_seeds: Option>, } -/// Extract #[rentfree_token(Variant, authority = [...])] attribute +/// Convert snake_case field name to CamelCase variant name +/// e.g., token_0_vault -> Token0Vault, vault -> Vault +fn snake_to_camel_case(s: &str) -> String { + s.split('_') + .map(|part| { + let mut chars = part.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().chain(chars).collect(), + } + }) + .collect() +} + +/// Extract #[rentfree_token(authority = [...])] attribute +/// Variant name is now derived from field name, not specified in attribute fn extract_rentfree_token_attr(attrs: &[syn::Attribute]) -> Option { for attr in attrs { if attr.path().is_ident("rentfree_token") { match &attr.meta { - // #[rentfree_token = Variant] + // #[rentfree_token = Variant] (deprecated but still supported) syn::Meta::NameValue(nv) => { if let Expr::Path(path) = &nv.value { if let Some(ident) = path.path.get_ident() { return Some(RentFreeTokenAttr { - variant_name: ident.clone(), + variant_name: Some(ident.clone()), authority_seeds: None, }); } } } - // #[rentfree_token(Variant)] or #[rentfree_token(Variant, authority = [...])] + // #[rentfree_token(authority = [...])] or #[rentfree_token(Variant, authority = [...])] syn::Meta::List(list) => { if let Ok(parsed) = parse_rentfree_token_list(&list.tokens) { return Some(parsed); } - // Fallback: try parsing as just an identifier + // Fallback: try parsing as just an identifier (deprecated) if let Ok(ident) = syn::parse2::(list.tokens.clone()) { return Some(RentFreeTokenAttr { - variant_name: ident, + variant_name: Some(ident), authority_seeds: None, }); } } - _ => {} + // #[rentfree_token] with no arguments + syn::Meta::Path(_) => { + return Some(RentFreeTokenAttr { + variant_name: None, + authority_seeds: None, + }); + } } } } None } -/// Parse rentfree_token(Variant, authority = [...]) content -fn parse_rentfree_token_list( - tokens: &proc_macro2::TokenStream, -) -> syn::Result { +/// Parse rentfree_token(authority = [...]) or rentfree_token(Variant, authority = [...]) content +fn parse_rentfree_token_list(tokens: &proc_macro2::TokenStream) -> syn::Result { use syn::parse::Parser; let parser = |input: syn::parse::ParseStream| -> syn::Result { - // First token is the variant name - let variant_name: Ident = input.parse()?; + let mut variant_name = None; let mut authority_seeds = None; - // Check for comma and additional args - while input.peek(syn::Token![,]) { - input.parse::()?; - - // Look for authority = [...] - if input.peek(Ident) { - let key: Ident = input.parse()?; - if key == "authority" { - input.parse::()?; - let array: syn::ExprArray = input.parse()?; - // Classify the authority seeds - let mut seeds = Vec::new(); - for elem in &array.elems { - if let Ok(seed) = classify_seed_expr(elem) { - seeds.push(seed); + // Check if first token is authority = [...] or a variant name + if input.peek(Ident) { + let ident: Ident = input.parse()?; + + if ident == "authority" { + // First token is authority, parse the seeds + input.parse::()?; + let array: syn::ExprArray = input.parse()?; + let mut seeds = Vec::new(); + for elem in &array.elems { + if let Ok(seed) = classify_seed_expr(elem) { + seeds.push(seed); + } + } + authority_seeds = Some(seeds); + } else { + // First token is variant name (deprecated but supported) + variant_name = Some(ident); + + // Check for comma and additional args + while input.peek(syn::Token![,]) { + input.parse::()?; + + // Look for authority = [...] + if input.peek(Ident) { + let key: Ident = input.parse()?; + if key == "authority" { + input.parse::()?; + let array: syn::ExprArray = input.parse()?; + let mut seeds = Vec::new(); + for elem in &array.elems { + if let Ok(seed) = classify_seed_expr(elem) { + seeds.push(seed); + } + } + authority_seeds = Some(seeds); } } - authority_seeds = Some(seeds); } } } diff --git a/sdk-libs/macros/src/compressible/decompress_context.rs b/sdk-libs/macros/src/compressible/decompress_context.rs index 1a42dfa147..fdf5f9dda1 100644 --- a/sdk-libs/macros/src/compressible/decompress_context.rs +++ b/sdk-libs/macros/src/compressible/decompress_context.rs @@ -110,7 +110,7 @@ pub fn generate_decompress_context_trait_impl( Ok(quote! { impl<#lifetime> light_sdk::compressible::DecompressContext<#lifetime> for DecompressAccountsIdempotent<#lifetime> { type CompressedData = RentFreeAccountData; - type PackedTokenData = light_ctoken_sdk::compat::PackedCTokenData<#packed_token_variant_ident>; + type PackedTokenData = light_token_sdk::compat::PackedCTokenData<#packed_token_variant_ident>; type CompressedMeta = light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; type SeedParams = (); @@ -131,7 +131,7 @@ pub fn generate_decompress_context_trait_impl( } fn token_program(&self) -> std::option::Option<&solana_account_info::AccountInfo<#lifetime>> { - self.ctoken_program.as_ref().map(|a| &**a) + self.light_token_program.as_ref().map(|a| &**a) } fn token_cpi_authority(&self) -> std::option::Option<&solana_account_info::AccountInfo<#lifetime>> { @@ -195,7 +195,7 @@ pub fn generate_decompress_context_trait_impl( post_system_accounts: &[solana_account_info::AccountInfo<#lifetime>], has_prior_context: bool, ) -> std::result::Result<(), solana_program_error::ProgramError> { - light_ctoken_sdk::compressible::process_decompress_tokens_runtime( + light_token_sdk::compressible::process_decompress_tokens_runtime( remaining_accounts, fee_payer, token_program, diff --git a/sdk-libs/macros/src/compressible/instructions.rs b/sdk-libs/macros/src/compressible/instructions.rs index 7c94029f95..9e90ec7387 100644 --- a/sdk-libs/macros/src/compressible/instructions.rs +++ b/sdk-libs/macros/src/compressible/instructions.rs @@ -362,658 +362,6 @@ impl Parse for InstructionDataSpec { } } -<<<<<<< HEAD -struct EnhancedMacroArgs { - account_types: Vec, - pda_seeds: Vec, - token_seeds: Vec, - instruction_data: Vec, -} - -impl Parse for EnhancedMacroArgs { - fn parse(input: ParseStream) -> Result { - let mut account_types = Vec::new(); - let mut pda_seeds = Vec::new(); - let mut token_seeds = Vec::new(); - let mut instruction_data = Vec::new(); - - let mut _item_count = 0; - while !input.is_empty() { - let ident: Ident = input.parse()?; - - if input.peek(Token![=]) { - let _eq: Token![=] = input.parse()?; - - if input.peek(syn::token::Paren) { - let content; - syn::parenthesized!(content in input); - let inside: TokenStream = content.parse()?; - let seed_spec: TokenSeedSpec = syn::parse2(quote! { #ident = (#inside) })?; - - let is_token_account = seed_spec.is_token.unwrap_or(false); - if is_token_account { - token_seeds.push(seed_spec); - } else { - pda_seeds.push(seed_spec); - account_types.push(ident); - } - } else { - let field_type: syn::Type = input.parse()?; - instruction_data.push(InstructionDataSpec { - field_name: ident, - field_type, - }); - } - } else { - account_types.push(ident); - } - - if input.peek(Token![,]) { - let _comma: Token![,] = input.parse()?; - } else { - break; - } - _item_count += 1; - } - Ok(EnhancedMacroArgs { - account_types, - pda_seeds, - token_seeds, - instruction_data, - }) - } -} - -#[allow(clippy::too_many_arguments)] -#[inline(never)] -pub fn add_compressible_instructions( - args: TokenStream, - mut module: ItemMod, -) -> Result { - let enhanced_args = match syn::parse2::(args.clone()) { - Ok(args) => args, - Err(e) => { - eprintln!("ERROR: Failed to parse macro args: {}", e); - eprintln!("Args were: {}", args); - return Err(e); - } - }; - - let account_types = enhanced_args.account_types; - let pda_seeds = Some(enhanced_args.pda_seeds); - let token_seeds = Some(enhanced_args.token_seeds); - let instruction_data = enhanced_args.instruction_data; - - if module.content.is_none() { - return Err(macro_error!(&module, "Module must have a body")); - } - - if account_types.is_empty() { - return Err(macro_error!( - &module, - "At least one account type must be specified" - )); - } - - let size_validation_checks = validate_compressed_account_sizes(&account_types)?; - - let content = module.content.as_mut().unwrap(); - - let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { - if !token_seed_specs.is_empty() { - crate::compressible::seed_providers::generate_token_account_variant_enum( - token_seed_specs, - )? - } else { - crate::compressible::utils::generate_empty_ctoken_enum() - } - } else { - crate::compressible::utils::generate_empty_ctoken_enum() - }; - - if let Some(ref token_seed_specs) = token_seeds { - for spec in token_seed_specs { - if spec.is_ata { - if !spec.seeds.is_empty() { - return Err(macro_error!( - &spec.variant, - "ATA variant '{}' must not have seeds - ATAs are derived from owner+mint only", - spec.variant - )); - } - if spec.authority.is_some() { - return Err(macro_error!( - &spec.variant, - "ATA variant '{}' must not have authority - ATAs are owned by user wallets", - spec.variant - )); - } - } else if spec.authority.is_none() { - return Err(macro_error!( - &spec.variant, - "Program-owned token account '{}' must specify authority = for compression signing. For user-owned ATAs, use is_ata flag instead.", - spec.variant - )); - } - } - } - - let mut account_types_stream = TokenStream::new(); - for (i, account_type) in account_types.iter().enumerate() { - if i > 0 { - account_types_stream.extend(quote! { , }); - } - account_types_stream.extend(quote! { #account_type }); - } - let enum_and_traits = - crate::compressible::variant_enum::compressed_account_variant(account_types_stream)?; - - // Generate SeedParams struct for instruction data fields - let seed_params_struct = { - let param_fields: Vec<_> = instruction_data - .iter() - .map(|spec| { - let field_name = &spec.field_name; - let field_type = &spec.field_type; - quote! { - pub #field_name: #field_type - } - }) - .collect(); - - quote! { - #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Clone, Debug, Default)] - pub struct SeedParams { - #(#param_fields,)* - } - } - }; - - let has_pda_seeds = pda_seeds.as_ref().map(|p| !p.is_empty()).unwrap_or(false); - let has_token_seeds = token_seeds.as_ref().map(|t| !t.is_empty()).unwrap_or(false); - - let instruction_variant = match (has_pda_seeds, has_token_seeds) { - (true, true) => InstructionVariant::Mixed, - (true, false) => InstructionVariant::PdaOnly, - (false, true) => InstructionVariant::TokenOnly, - (false, false) => { - return Err(macro_error!( - &module, - "At least one PDA or token seed specification must be provided" - )) - } - }; - - let error_codes = generate_error_codes(instruction_variant)?; - - let required_accounts = extract_required_accounts_from_seeds(&pda_seeds, &token_seeds)?; - - let decompress_accounts = - generate_decompress_accounts_struct(&required_accounts, instruction_variant)?; - - let pda_seed_provider_impls: Result> = account_types - .iter() - .map(|name| { - let name_str = name.to_string(); - let spec = if let Some(ref pda_seed_specs) = pda_seeds { - pda_seed_specs - .iter() - .find(|s| s.variant == name_str) - .ok_or_else(|| { - macro_error!( - name, - "No seed specification for account type '{}'. All accounts must have seed specifications.", - name_str - ) - })? - } else { - return Err(macro_error!( - name, - "No seed specifications provided. Use: AccountType = (\"seed\", data.field)" - )); - }; - let seed_derivation = - generate_pda_seed_derivation_for_trait(spec, &instruction_data)?; - Ok(quote! { - impl<'info> light_sdk::compressible::PdaSeedDerivation, SeedParams> for #name { - fn derive_pda_seeds_with_accounts( - &self, - program_id: &solana_pubkey::Pubkey, - accounts: &DecompressAccountsIdempotent<'info>, - seed_params: &SeedParams, - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { - #seed_derivation - } - } - }) - }) - .collect(); - let pda_seed_provider_impls = pda_seed_provider_impls?; - - let helper_packed_fns: Vec<_> = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); - let func_name = format_ident!("handle_packed_{}", name); - quote! { - #[inline(never)] - #[allow(clippy::too_many_arguments)] - fn #func_name<'a, 'b, 'info>( - accounts: &DecompressAccountsIdempotent<'info>, - cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, 'info>, - address_space: solana_pubkey::Pubkey, - solana_accounts: &[solana_account_info::AccountInfo<'info>], - i: usize, - packed: &#packed_name, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - post_system_accounts: &[solana_account_info::AccountInfo<'info>], - compressed_pda_infos: &mut Vec, - seed_accounts: &DecompressAccountsIdempotent<'info>, - seed_params: &SeedParams, - ) -> std::result::Result<(), solana_program_error::ProgramError> { - light_sdk::compressible::handle_packed_pda_variant::<#name, #packed_name, DecompressAccountsIdempotent<'info>, SeedParams>( - accounts.rent_sponsor.as_ref(), - cpi_accounts, - address_space, - &solana_accounts[i], - i, - packed, - meta, - post_system_accounts, - compressed_pda_infos, - &crate::ID, - seed_accounts, - std::option::Option::Some(seed_params), - ) - } - } - }).collect(); - - let call_unpacked_arms: Vec<_> = account_types.iter().map(|name| { - quote! { - CompressedAccountVariant::#name(_) => { - unreachable!("Unpacked variants should not be present during decompression - accounts are always packed in-flight"); - } - } - }).collect(); - let call_packed_arms: Vec<_> = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); - let func_name = format_ident!("handle_packed_{}", name); - quote! { - CompressedAccountVariant::#packed_name(packed) => { - match #func_name(accounts, &cpi_accounts, address_space, solana_accounts, i, &packed, &meta, post_system_accounts, &mut compressed_pda_infos, accounts, seed_params) { - std::result::Result::Ok(()) => {}, - std::result::Result::Err(e) => return std::result::Result::Err(e), - } - } - } - }).collect(); - - let trait_impls: syn::ItemMod = syn::parse_quote! { - mod __trait_impls { - use super::*; - - impl light_sdk::compressible::HasTokenVariant for CompressedAccountData { - fn is_packed_token(&self) -> bool { - matches!(self.data, CompressedAccountVariant::PackedCTokenData(_)) - } - } - - impl light_sdk::compressible::TokenSeedProvider for CTokenAccountVariant { - type Accounts<'info> = DecompressAccountsIdempotent<'info>; - - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [solana_account_info::AccountInfo<'info>], - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), anchor_lang::prelude::ProgramError> { - use super::ctoken_seed_system::{ - CTokenSeedContext, - CTokenSeedProvider as LocalProvider, - }; - let ctx = CTokenSeedContext { - accounts, - remaining_accounts, - }; - LocalProvider::get_seeds(self, &ctx).map_err(|e: anchor_lang::error::Error| -> anchor_lang::prelude::ProgramError { e.into() }) - } - - fn get_authority_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [solana_account_info::AccountInfo<'info>], - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), anchor_lang::prelude::ProgramError> { - use super::ctoken_seed_system::{ - CTokenSeedContext, - CTokenSeedProvider as LocalProvider, - }; - let ctx = CTokenSeedContext { - accounts, - remaining_accounts, - }; - LocalProvider::get_authority_seeds(self, &ctx).map_err(|e: anchor_lang::error::Error| -> anchor_lang::prelude::ProgramError { e.into() }) - } - } - - impl light_token_sdk::compressible::TokenSeedProvider for CTokenAccountVariant { - type Accounts<'info> = DecompressAccountsIdempotent<'info>; - - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [solana_account_info::AccountInfo<'info>], - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { - use super::ctoken_seed_system::{ - CTokenSeedContext, - CTokenSeedProvider as LocalProvider, - }; - let ctx = CTokenSeedContext { - accounts, - remaining_accounts, - }; - LocalProvider::get_seeds(self, &ctx) - .map_err(|e: anchor_lang::error::Error| { - let program_error: anchor_lang::prelude::ProgramError = e.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - }) - } - - fn get_authority_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [solana_account_info::AccountInfo<'info>], - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { - use super::ctoken_seed_system::{ - CTokenSeedContext, - CTokenSeedProvider as LocalProvider, - }; - let ctx = CTokenSeedContext { - accounts, - remaining_accounts, - }; - LocalProvider::get_authority_seeds(self, &ctx) - .map_err(|e: anchor_lang::error::Error| { - let program_error: anchor_lang::prelude::ProgramError = e.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - }) - } - } - } - }; - - let ctoken_trait_system: syn::ItemMod = syn::parse_quote! { - pub mod ctoken_seed_system { - use super::*; - - pub struct CTokenSeedContext<'a, 'info> { - pub accounts: &'a DecompressAccountsIdempotent<'info>, - pub remaining_accounts: &'a [solana_account_info::AccountInfo<'info>], - } - - pub trait CTokenSeedProvider { - fn get_seeds<'a, 'info>( - &self, - ctx: &CTokenSeedContext<'a, 'info>, - ) -> Result<(Vec>, solana_pubkey::Pubkey)>; - - fn get_authority_seeds<'a, 'info>( - &self, - ctx: &CTokenSeedContext<'a, 'info>, - ) -> Result<(Vec>, solana_pubkey::Pubkey)>; - } - } - }; - - let helpers_module: syn::ItemMod = { - let helper_packed_fns = helper_packed_fns.clone(); - let call_unpacked_arms = call_unpacked_arms.clone(); - let call_packed_arms = call_packed_arms.clone(); - syn::parse_quote! { - mod __macro_helpers { - use super::*; - use crate::state::*; // Import Packed* types from state module - #(#helper_packed_fns)* - #[inline(never)] - pub fn collect_pda_and_token<'a, 'b, 'info>( - accounts: &DecompressAccountsIdempotent<'info>, - cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, 'info>, - address_space: solana_pubkey::Pubkey, - compressed_accounts: Vec, - solana_accounts: &[solana_account_info::AccountInfo<'info>], - seed_params: &SeedParams, - ) -> std::result::Result<( - Vec, - Vec<( - light_token_sdk::compat::PackedCTokenData, - light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - )>, - ), solana_program_error::ProgramError> { - let post_system_offset = cpi_accounts.system_accounts_end_offset(); - let all_infos = cpi_accounts.account_infos(); - let post_system_accounts = &all_infos[post_system_offset..]; - let estimated_capacity = compressed_accounts.len(); - let mut compressed_pda_infos = Vec::with_capacity(estimated_capacity); - let mut compressed_token_accounts: Vec<( - light_token_sdk::compat::PackedCTokenData, - light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - )> = Vec::with_capacity(estimated_capacity); - - for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { - let meta = compressed_data.meta; - match compressed_data.data { - #(#call_unpacked_arms)* - #(#call_packed_arms)* - CompressedAccountVariant::PackedCTokenData(mut data) => { - data.token_data.version = 3; - compressed_token_accounts.push((data, meta)); - } - CompressedAccountVariant::CTokenData(_) => { - unreachable!(); - } - } - } - - std::result::Result::Ok((compressed_pda_infos, compressed_token_accounts)) - } - } - } - }; - - let token_variant_name = format_ident!("CTokenAccountVariant"); - - let decompress_context_impl = generate_decompress_context_impl( - instruction_variant, - account_types.clone(), - token_variant_name, - )?; - let decompress_processor_fn = - generate_process_decompress_accounts_idempotent(instruction_variant, &instruction_data)?; - let decompress_instruction = - generate_decompress_instruction_entrypoint(instruction_variant, &instruction_data)?; - - let compress_accounts: syn::ItemStruct = match instruction_variant { - InstructionVariant::PdaOnly => unreachable!(), - InstructionVariant::TokenOnly => unreachable!(), - InstructionVariant::Mixed => 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>, - } - }, - }; - - let compress_context_impl = - generate_compress_context_impl(instruction_variant, account_types.clone())?; - let compress_processor_fn = generate_process_compress_accounts_idempotent(instruction_variant)?; - let compress_instruction = generate_compress_instruction_entrypoint(instruction_variant)?; - - let processor_module: syn::ItemMod = syn::parse_quote! { - mod __processor_functions { - use super::*; - #decompress_processor_fn - #compress_processor_fn - } - }; - - let init_config_accounts: syn::ItemStruct = syn::parse_quote! { - #[derive(Accounts)] - pub struct InitializeCompressionConfig<'info> { - #[account(mut)] - pub payer: Signer<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub config: AccountInfo<'info>, - /// CHECK: Checked by SDK - pub program_data: AccountInfo<'info>, - pub authority: Signer<'info>, - pub system_program: Program<'info, System>, - } - }; - - let update_config_accounts: syn::ItemStruct = syn::parse_quote! { - #[derive(Accounts)] - pub struct UpdateCompressionConfig<'info> { - /// CHECK: Checked by SDK - #[account(mut)] - pub config: AccountInfo<'info>, - /// CHECK: Checked by SDK - pub authority: Signer<'info>, - } - }; - - let init_config_instruction: syn::ItemFn = syn::parse_quote! { - #[inline(never)] - #[allow(clippy::too_many_arguments)] - pub fn initialize_compression_config<'info>( - ctx: Context<'_, '_, '_, 'info, InitializeCompressionConfig<'info>>, - write_top_up: u32, - rent_sponsor: Pubkey, - compression_authority: Pubkey, - rent_config: light_compressible::rent::RentConfig, - address_space: Vec, - ) -> Result<()> { - light_sdk::compressible::process_initialize_compression_config_checked( - &ctx.accounts.config.to_account_info(), - &ctx.accounts.authority.to_account_info(), - &ctx.accounts.program_data.to_account_info(), - &rent_sponsor, - &compression_authority, - rent_config, - write_top_up, - address_space, - 0, - &ctx.accounts.payer.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - &crate::ID, - )?; - Ok(()) - } - }; - - let update_config_instruction: syn::ItemFn = syn::parse_quote! { - #[inline(never)] - #[allow(clippy::too_many_arguments)] - pub fn update_compression_config<'info>( - ctx: Context<'_, '_, '_, 'info, UpdateCompressionConfig<'info>>, - new_rent_sponsor: Option, - new_compression_authority: Option, - new_rent_config: Option, - new_write_top_up: Option, - new_address_space: Option>, - new_update_authority: Option, - ) -> Result<()> { - light_sdk::compressible::process_update_compression_config( - ctx.accounts.config.as_ref(), - ctx.accounts.authority.as_ref(), - new_update_authority.as_ref(), - new_rent_sponsor.as_ref(), - new_compression_authority.as_ref(), - new_rent_config, - new_write_top_up, - new_address_space, - &crate::ID, - )?; - Ok(()) - } - }; - - // Insert SeedParams struct - let seed_params_item: Item = syn::parse2(seed_params_struct)?; - content.1.push(seed_params_item); - - content.1.push(Item::Struct(decompress_accounts)); - content.1.push(Item::Mod(helpers_module)); - content.1.push(Item::Mod(ctoken_trait_system)); - content.1.push(Item::Mod(trait_impls)); - content.1.push(Item::Mod(decompress_context_impl)); - content.1.push(Item::Mod(processor_module)); - content.1.push(Item::Fn(decompress_instruction)); - content.1.push(Item::Struct(compress_accounts)); - content.1.push(Item::Mod(compress_context_impl)); - content.1.push(Item::Fn(compress_instruction)); - content.1.push(Item::Struct(init_config_accounts)); - content.1.push(Item::Struct(update_config_accounts)); - content.1.push(Item::Fn(init_config_instruction)); - content.1.push(Item::Fn(update_config_instruction)); - - if let Some(ref seeds) = token_seeds { - if !seeds.is_empty() { - let impl_code = - crate::compressible::seed_providers::generate_token_seed_provider_implementation( - seeds, - )?; - let ctoken_impl: syn::ItemImpl = syn::parse2(impl_code).map_err(|e| { - syn::Error::new_spanned( - &seeds[0].variant, - format!("Failed to parse ctoken implementation: {}", e), - ) - })?; - content.1.push(Item::Impl(ctoken_impl)); - } - } - - let client_seed_functions = - crate::compressible::seed_providers::generate_client_seed_functions( - &account_types, - &pda_seeds, - &token_seeds, - &instruction_data, - )?; - - // Add allow attribute to module itself to suppress clippy warnings - module.attrs.push(syn::parse_quote! { - #[allow(clippy::too_many_arguments)] - }); - - Ok(quote! { - #size_validation_checks - #error_codes - #ctoken_enum - #enum_and_traits - #(#pda_seed_provider_impls)* - #[allow(non_snake_case)] - #module - #client_seed_functions - }) -} - -======= ->>>>>>> a606eb113 (wip) pub fn generate_decompress_context_impl( _variant: InstructionVariant, pda_ctx_seeds: Vec, @@ -1434,7 +782,7 @@ fn generate_decompress_accounts_struct( quote! { /// CHECK: #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] - pub ctoken_program: Option> + pub light_token_program: Option> }, quote! { /// CHECK: @@ -1457,7 +805,7 @@ fn generate_decompress_accounts_struct( "rent_sponsor", "ctoken_rent_sponsor", "config", - "ctoken_program", + "light_token_program", "ctoken_cpi_authority", "ctoken_config", ]; @@ -1805,14 +1153,14 @@ fn generate_from_extracted_seeds( use super::*; impl light_sdk::compressible::HasTokenVariant for RentFreeAccountData { - fn is_packed_ctoken(&self) -> bool { + fn is_packed_token(&self) -> bool { matches!(self.data, RentFreeAccountVariant::PackedCTokenData(_)) } } } }; - let token_variant_name = format_ident!("CTokenAccountVariant"); + let token_variant_name = format_ident!("TokenAccountVariant"); let decompress_context_impl = generate_decompress_context_impl( instruction_variant, @@ -2091,7 +1439,7 @@ pub fn compressible_program_impl( } found_pda_seeds.push(TokenSeedSpec { - variant: pda.inner_type.clone(), + variant: pda.variant_name.clone(), _eq: syn::parse_quote!(=), is_token: Some(false), seeds: seed_elements, diff --git a/sdk-libs/macros/src/compressible/seed_providers.rs b/sdk-libs/macros/src/compressible/seed_providers.rs index ed7cbf4973..604cf8c1e3 100644 --- a/sdk-libs/macros/src/compressible/seed_providers.rs +++ b/sdk-libs/macros/src/compressible/seed_providers.rs @@ -12,13 +12,21 @@ fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { let mut seen = std::collections::HashSet::new(); // Helper to extract ctx.* from a SeedElement - fn extract_from_seed(seed: &SeedElement, ctx_fields: &mut Vec, seen: &mut std::collections::HashSet) { + fn extract_from_seed( + seed: &SeedElement, + ctx_fields: &mut Vec, + seen: &mut std::collections::HashSet, + ) { if let SeedElement::Expression(expr) = seed { extract_ctx_from_expr(expr, ctx_fields, seen); } } - fn extract_ctx_from_expr(expr: &syn::Expr, ctx_fields: &mut Vec, seen: &mut std::collections::HashSet) { + fn extract_ctx_from_expr( + expr: &syn::Expr, + ctx_fields: &mut Vec, + seen: &mut std::collections::HashSet, + ) { if let syn::Expr::Field(field_expr) = expr { if let syn::Member::Named(field_name) = &field_expr.member { // Check for ctx.accounts.field pattern @@ -30,7 +38,13 @@ fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { if segment.ident == "ctx" { let field_name_str = field_name.to_string(); // Skip standard fields - if !matches!(field_name_str.as_str(), "fee_payer" | "rent_sponsor" | "config" | "compression_authority") { + if !matches!( + field_name_str.as_str(), + "fee_payer" + | "rent_sponsor" + | "config" + | "compression_authority" + ) { if seen.insert(field_name_str) { ctx_fields.push(field_name.clone()); } @@ -46,7 +60,10 @@ fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { if let Some(segment) = path.path.segments.first() { if segment.ident == "ctx" { let field_name_str = field_name.to_string(); - if !matches!(field_name_str.as_str(), "fee_payer" | "rent_sponsor" | "config" | "compression_authority") { + if !matches!( + field_name_str.as_str(), + "fee_payer" | "rent_sponsor" | "config" | "compression_authority" + ) { if seen.insert(field_name_str) { ctx_fields.push(field_name.clone()); } @@ -126,19 +143,26 @@ pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Re if ctx_fields.is_empty() { quote! { - CTokenAccountVariant::#variant_name => PackedCTokenAccountVariant::#variant_name, + 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(); + 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! { - CTokenAccountVariant::#variant_name { #(#field_bindings,)* } => { + TokenAccountVariant::#variant_name { #(#field_bindings,)* } => { #(#pack_stmts)* - PackedCTokenAccountVariant::#variant_name { #(#idx_fields,)* } + PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } } } } @@ -151,25 +175,32 @@ pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Re if ctx_fields.is_empty() { quote! { - PackedCTokenAccountVariant::#variant_name => Ok(CTokenAccountVariant::#variant_name), + 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)| { - // Dereference idx since match pattern gives us &u8 - quote! { - let #field = *remaining_accounts - .get(*#idx as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? - .key; - } - }).collect(); + 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)| { + // Dereference idx since match pattern gives us &u8 + 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(); quote! { - PackedCTokenAccountVariant::#variant_name { #(#idx_fields,)* } => { + PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } => { #(#unpack_stmts)* - Ok(CTokenAccountVariant::#variant_name { #(#field_names,)* }) + Ok(TokenAccountVariant::#variant_name { #(#field_names,)* }) } } } @@ -177,17 +208,17 @@ pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Re Ok(quote! { #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] - pub enum CTokenAccountVariant { + pub enum TokenAccountVariant { #(#unpacked_variants)* } #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] - pub enum PackedCTokenAccountVariant { + pub enum PackedTokenAccountVariant { #(#packed_variants)* } - impl light_ctoken_sdk::pack::Pack for CTokenAccountVariant { - type Packed = PackedCTokenAccountVariant; + impl light_token_sdk::pack::Pack for TokenAccountVariant { + type Packed = PackedTokenAccountVariant; fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { match self { @@ -196,8 +227,8 @@ pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Re } } - impl light_ctoken_sdk::pack::Unpack for PackedCTokenAccountVariant { - type Unpacked = CTokenAccountVariant; + impl light_token_sdk::pack::Unpack for PackedTokenAccountVariant { + type Unpacked = TokenAccountVariant; fn unpack( &self, @@ -209,9 +240,9 @@ pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Re } } - impl light_sdk::compressible::IntoCTokenVariant for CTokenAccountVariant { - fn into_ctoken_variant(self, token_data: light_ctoken_sdk::compat::TokenData) -> RentFreeAccountVariant { - RentFreeAccountVariant::CTokenData(light_ctoken_sdk::compat::CTokenData { + 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, }) @@ -220,7 +251,7 @@ pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Re }) } -/// Phase 8: Generate CTokenSeedProvider impl that uses self.field instead of ctx.accounts.field +/// Phase 8: Generate TokenSeedProvider impl that uses self.field instead of ctx.accounts.field pub fn generate_ctoken_seed_provider_implementation( token_seeds: &[TokenSeedSpec], ) -> Result { @@ -233,10 +264,10 @@ pub fn generate_ctoken_seed_provider_implementation( // Build match pattern with destructuring if there are ctx fields let pattern = if ctx_fields.is_empty() { - quote! { CTokenAccountVariant::#variant_name } + quote! { TokenAccountVariant::#variant_name } } else { let field_names: Vec<_> = ctx_fields.iter().collect(); - quote! { CTokenAccountVariant::#variant_name { #(#field_names,)* } } + quote! { TokenAccountVariant::#variant_name { #(#field_names,)* } } }; // Build seed refs for get_seeds - use self.field directly for ctx.* seeds @@ -359,7 +390,7 @@ pub fn generate_ctoken_seed_provider_implementation( // Phase 8: New trait signature - no ctx/accounts parameter needed Ok(quote! { - impl light_sdk::compressible::CTokenSeedProvider for CTokenAccountVariant { + impl light_sdk::compressible::TokenSeedProvider for TokenAccountVariant { fn get_seeds( &self, program_id: &solana_pubkey::Pubkey, diff --git a/sdk-libs/macros/src/compressible/utils.rs b/sdk-libs/macros/src/compressible/utils.rs index 3b337c232e..35c09e5d93 100644 --- a/sdk-libs/macros/src/compressible/utils.rs +++ b/sdk-libs/macros/src/compressible/utils.rs @@ -104,13 +104,13 @@ pub(crate) fn is_pubkey_type(ty: &Type) -> bool { } } -/// Generates an empty CTokenAccountVariant enum. +/// Generates an empty TokenAccountVariant enum. /// /// This is used when no token accounts are specified in compressible instructions. pub(crate) fn generate_empty_ctoken_enum() -> proc_macro2::TokenStream { quote::quote! { #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Debug, Clone, Copy)] #[repr(u8)] - pub enum CTokenAccountVariant {} + pub enum TokenAccountVariant {} } } diff --git a/sdk-libs/macros/src/compressible/variant_enum.rs b/sdk-libs/macros/src/compressible/variant_enum.rs index 177558b54c..88aa578c36 100644 --- a/sdk-libs/macros/src/compressible/variant_enum.rs +++ b/sdk-libs/macros/src/compressible/variant_enum.rs @@ -60,24 +60,22 @@ pub fn compressed_account_variant_with_ctx_seeds( } }); - // Phase 8: PackedCTokenData uses PackedCTokenAccountVariant (with idx fields) - // CTokenData uses CTokenAccountVariant (with Pubkey 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)* -<<<<<<< HEAD - PackedCTokenData(light_token_sdk::compat::PackedCTokenData), - CTokenData(light_token_sdk::compat::CTokenData), -======= - PackedCTokenData(light_ctoken_sdk::compat::PackedCTokenData), - CTokenData(light_ctoken_sdk::compat::CTokenData), ->>>>>>> a606eb113 (wip) + PackedCTokenData(light_token_sdk::compat::PackedCTokenData), + CTokenData(light_token_sdk::compat::CTokenData), } }; let first_type = account_types[0]; - let first_ctx_fields = ctx_seeds_map.get(&first_type.to_string()).copied().unwrap_or(&[]); + let first_ctx_fields = ctx_seeds_map + .get(&first_type.to_string()) + .copied() + .unwrap_or(&[]); let first_default_ctx_fields = first_ctx_fields.iter().map(|field| { quote! { #field: Pubkey::default() } }); @@ -249,12 +247,8 @@ pub fn compressed_account_variant_with_ctx_seeds( #(#pack_match_arms)* Self::PackedCTokenData(_) => unreachable!(), Self::CTokenData(data) => { -<<<<<<< HEAD - Self::PackedCTokenData(light_token_sdk::pack::Pack::pack(data, remaining_accounts)) -======= // Use ctoken-sdk's Pack trait for CTokenData - Self::PackedCTokenData(light_ctoken_sdk::pack::Pack::pack(data, remaining_accounts)) ->>>>>>> a606eb113 (wip) + Self::PackedCTokenData(light_token_sdk::pack::Pack::pack(data, remaining_accounts)) } } } diff --git a/sdk-libs/macros/src/finalize/codegen.rs b/sdk-libs/macros/src/finalize/codegen.rs index df40420c12..8cd36926a2 100644 --- a/sdk-libs/macros/src/finalize/codegen.rs +++ b/sdk-libs/macros/src/finalize/codegen.rs @@ -95,11 +95,11 @@ pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream .map(|f| quote! { #f }) .unwrap_or_else(|| quote! { ctoken_rent_sponsor }); - let ctoken_program = parsed + let light_token_program = parsed .ctoken_program_field .as_ref() .map(|f| quote! { #f }) - .unwrap_or_else(|| quote! { ctoken_program }); + .unwrap_or_else(|| quote! { light_token_program }); let ctoken_cpi_authority = parsed .ctoken_cpi_authority_field @@ -118,7 +118,7 @@ pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream &compression_config, &ctoken_config, &ctoken_rent_sponsor, - &ctoken_program, + &light_token_program, &ctoken_cpi_authority, ) } else if has_mints { @@ -129,7 +129,7 @@ pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream &fee_payer, &ctoken_config, &ctoken_rent_sponsor, - &ctoken_program, + &light_token_program, &ctoken_cpi_authority, ) } else if has_pdas { @@ -182,7 +182,7 @@ fn generate_pre_init_pdas_and_mints( compression_config: &TokenStream, ctoken_config: &TokenStream, ctoken_rent_sponsor: &TokenStream, - ctoken_program: &TokenStream, + light_token_program: &TokenStream, ctoken_cpi_authority: &TokenStream, ) -> TokenStream { let (compress_blocks, new_addr_idents) = @@ -279,26 +279,23 @@ fn generate_pre_init_pdas_and_mints( let __tree_pubkey: solana_pubkey::Pubkey = light_sdk::light_account_checks::AccountInfoTrait::pubkey(address_tree); let mint_signer_key = self.#mint_signer.to_account_info().key; - let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address( - mint_signer_key, - &__tree_pubkey, - ); - let (mint_pda, cmint_bump) = light_ctoken_sdk::ctoken::find_cmint_address(mint_signer_key); + let (mint_pda, _cmint_bump) = light_token_sdk::token::find_mint_address(mint_signer_key); - let __proof: light_ctoken_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() + let __proof: light_token_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() .expect("proof is required for mint creation"); let __freeze_authority: Option = #freeze_authority_tokens; // Build compressed mint instruction data - let compressed_mint_data = light_ctoken_interface::instructions::mint_action::CompressedMintInstructionData { + let compressed_mint_data = light_token_interface::instructions::mint_action::CompressedMintInstructionData { supply: 0, decimals: #decimals, - metadata: light_ctoken_interface::state::CompressedMintMetadata { + metadata: light_token_interface::state::CompressedMintMetadata { version: 3, mint: mint_pda.to_bytes().into(), cmint_decompressed: false, - compressed_address: compression_address, + mint_signer: mint_signer_key.to_bytes(), + bump: _cmint_bump, }, mint_authority: Some((*self.#authority.to_account_info().key).to_bytes().into()), freeze_authority: __freeze_authority.map(|a| a.to_bytes().into()), @@ -306,17 +303,16 @@ fn generate_pre_init_pdas_and_mints( }; // Build mint action instruction data with decompress - let mut instruction_data = light_ctoken_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( + let mut instruction_data = light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( __tree_info.root_index, __proof, compressed_mint_data, ) - .with_decompress_mint(light_ctoken_interface::instructions::mint_action::DecompressMintAction { - cmint_bump, + .with_decompress_mint(light_token_interface::instructions::mint_action::DecompressMintAction { rent_payment: #rent_payment_tokens, write_top_up: #write_top_up_tokens, }) - .with_cpi_context(light_ctoken_interface::instructions::mint_action::CpiContext { + .with_cpi_context(light_token_interface::instructions::mint_action::CpiContext { address_tree_pubkey: __tree_pubkey.to_bytes(), set_context: false, first_set_context: false, // PDAs already wrote to context @@ -331,14 +327,14 @@ fn generate_pre_init_pdas_and_mints( }); // Build account metas with compressible CMint - let mut meta_config = light_ctoken_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( + let mut meta_config = light_token_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( *self.#fee_payer.to_account_info().key, *self.#authority.to_account_info().key, *mint_signer_key, __tree_pubkey, *output_queue.key, ) - .with_compressible_cmint( + .with_compressible_mint( mint_pda, *self.#ctoken_config.to_account_info().key, *self.#ctoken_rent_sponsor.to_account_info().key, @@ -353,7 +349,7 @@ fn generate_pre_init_pdas_and_mints( .map_err(|e| light_sdk::error::LightSdkError::Borsh)?; let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { - program_id: solana_pubkey::Pubkey::new_from_array(light_ctoken_interface::CTOKEN_PROGRAM_ID), + program_id: solana_pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), accounts: account_metas, data: ix_data, }; @@ -362,7 +358,7 @@ fn generate_pre_init_pdas_and_mints( // Include all accounts needed for mint_action with decompress let mut account_infos = cpi_accounts.to_account_infos(); // Add ctoken-specific accounts that aren't in the Light System CPI accounts - account_infos.push(self.#ctoken_program.to_account_info()); + account_infos.push(self.#light_token_program.to_account_info()); account_infos.push(self.#ctoken_cpi_authority.to_account_info()); account_infos.push(self.#mint_field_ident.to_account_info()); account_infos.push(self.#ctoken_config.to_account_info()); @@ -393,7 +389,7 @@ fn generate_pre_init_mints_only( fee_payer: &TokenStream, ctoken_config: &TokenStream, ctoken_rent_sponsor: &TokenStream, - ctoken_program: &TokenStream, + light_token_program: &TokenStream, ctoken_cpi_authority: &TokenStream, ) -> TokenStream { // Get the first mint (we only support one mint currently) @@ -448,26 +444,23 @@ fn generate_pre_init_mints_only( let __tree_pubkey: solana_pubkey::Pubkey = light_sdk::light_account_checks::AccountInfoTrait::pubkey(address_tree); let mint_signer_key = self.#mint_signer.to_account_info().key; - let compression_address = light_ctoken_sdk::ctoken::derive_cmint_compressed_address( - mint_signer_key, - &__tree_pubkey, - ); - let (mint_pda, cmint_bump) = light_ctoken_sdk::ctoken::find_cmint_address(mint_signer_key); + let (mint_pda, _cmint_bump) = light_token_sdk::token::find_mint_address(mint_signer_key); - let __proof: light_ctoken_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() + let __proof: light_token_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() .expect("proof is required for mint creation"); let __freeze_authority: Option = #freeze_authority_tokens; // Build compressed mint instruction data - let compressed_mint_data = light_ctoken_interface::instructions::mint_action::CompressedMintInstructionData { + let compressed_mint_data = light_token_interface::instructions::mint_action::CompressedMintInstructionData { supply: 0, decimals: #decimals, - metadata: light_ctoken_interface::state::CompressedMintMetadata { + metadata: light_token_interface::state::CompressedMintMetadata { version: 3, mint: mint_pda.to_bytes().into(), cmint_decompressed: false, - compressed_address: compression_address, + mint_signer: mint_signer_key.to_bytes(), + bump: _cmint_bump, }, mint_authority: Some((*self.#authority.to_account_info().key).to_bytes().into()), freeze_authority: __freeze_authority.map(|a| a.to_bytes().into()), @@ -475,26 +468,25 @@ fn generate_pre_init_mints_only( }; // Build mint action instruction data with decompress (no CPI context) - let instruction_data = light_ctoken_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( + let instruction_data = light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( __tree_info.root_index, __proof, compressed_mint_data, ) - .with_decompress_mint(light_ctoken_interface::instructions::mint_action::DecompressMintAction { - cmint_bump, + .with_decompress_mint(light_token_interface::instructions::mint_action::DecompressMintAction { rent_payment: #rent_payment_tokens, write_top_up: #write_top_up_tokens, }); // Build account metas with compressible CMint - let meta_config = light_ctoken_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( + let meta_config = light_token_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( *self.#fee_payer.to_account_info().key, *self.#authority.to_account_info().key, *mint_signer_key, __tree_pubkey, *output_queue.key, ) - .with_compressible_cmint( + .with_compressible_mint( mint_pda, *self.#ctoken_config.to_account_info().key, *self.#ctoken_rent_sponsor.to_account_info().key, @@ -507,7 +499,7 @@ fn generate_pre_init_mints_only( .map_err(|e| light_sdk::error::LightSdkError::Borsh)?; let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { - program_id: solana_pubkey::Pubkey::new_from_array(light_ctoken_interface::CTOKEN_PROGRAM_ID), + program_id: solana_pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), accounts: account_metas, data: ix_data, }; @@ -515,7 +507,7 @@ fn generate_pre_init_mints_only( // Build account infos and invoke let mut account_infos = cpi_accounts.to_account_infos(); // Add ctoken-specific accounts - account_infos.push(self.#ctoken_program.to_account_info()); + account_infos.push(self.#light_token_program.to_account_info()); account_infos.push(self.#ctoken_cpi_authority.to_account_info()); account_infos.push(self.#mint_field_ident.to_account_info()); account_infos.push(self.#ctoken_config.to_account_info()); diff --git a/sdk-libs/macros/src/finalize/parse.rs b/sdk-libs/macros/src/finalize/parse.rs index 6296cf3892..3167f30846 100644 --- a/sdk-libs/macros/src/finalize/parse.rs +++ b/sdk-libs/macros/src/finalize/parse.rs @@ -275,7 +275,7 @@ pub fn parse_compressible_struct(input: &DeriveInput) -> Result TokenStream { /// seeds = [b"vault", cmint.key().as_ref()], /// bump /// )] -/// #[rentfree_token(Vault, authority = [b"vault_authority"])] +/// // Variant name derived from field name: vault -> Vault +/// #[rentfree_token(authority = [b"vault_authority"])] /// pub vault: UncheckedAccount<'info>, /// } /// ``` diff --git a/sdk-libs/program-test/src/compressible.rs b/sdk-libs/program-test/src/compressible.rs index 67efeec166..6406f1b58b 100644 --- a/sdk-libs/program-test/src/compressible.rs +++ b/sdk-libs/program-test/src/compressible.rs @@ -52,7 +52,7 @@ fn extract_compression_info(data: &[u8]) -> Option<(CompressionInfo, u8, bool)> match account_type { ACCOUNT_TYPE_TOKEN_ACCOUNT => { - let (ctoken, _) = CToken::zero_copy_at(data).ok()?; + let (ctoken, _) = Token::zero_copy_at(data).ok()?; let ext = ctoken.get_compressible_extension()?; let compression_info = CompressionInfo { @@ -414,14 +414,14 @@ async fn compress_cmint_forester( use light_client::indexer::Indexer; use light_compressed_account::instruction_data::traits::LightInstructionData; use light_compressible::config::CompressibleConfig; - use light_ctoken_interface::{ + use light_token_interface::{ instructions::mint_action::{ CompressAndCloseCMintAction, CompressedMintWithContext, MintActionCompressedInstructionData, }, - CTOKEN_PROGRAM_ID, + LIGHT_TOKEN_PROGRAM_ID, }; - use light_ctoken_sdk::compressed_token::mint_action::MintActionMetaConfig; + use light_token_sdk::compressed_token::mint_action::MintActionMetaConfig; use solana_sdk::signature::Signer; // Get CMint account data @@ -434,7 +434,7 @@ async fn compress_cmint_forester( BorshDeserialize::deserialize(&mut cmint_account.data.as_slice()) .map_err(|e| RpcError::CustomError(format!("Failed to deserialize CMint: {:?}", e)))?; - let compressed_mint_address = cmint.metadata.compressed_address; + let compressed_mint_address = cmint.metadata.compressed_address(); let rent_sponsor = Pubkey::from(cmint.compression.rent_sponsor); // Get the compressed mint account from indexer @@ -479,7 +479,7 @@ async fn compress_cmint_forester( let state_tree_info = rpc_proof_result.accounts[0].tree_info; // Build account metas - authority can be anyone for permissionless CompressAndCloseCMint - let config_address = CompressibleConfig::ctoken_v1_config_pda(); + let config_address = CompressibleConfig::light_token_v1_config_pda(); let meta_config = MintActionMetaConfig::new( payer.pubkey(), payer.pubkey(), // authority doesn't matter for CompressAndCloseCMint @@ -487,7 +487,7 @@ async fn compress_cmint_forester( state_tree_info.queue, state_tree_info.queue, ) - .with_compressible_cmint(cmint_pubkey, config_address, rent_sponsor); + .with_compressible_mint(cmint_pubkey, config_address, rent_sponsor); let account_metas = meta_config.to_account_metas(); @@ -498,7 +498,7 @@ async fn compress_cmint_forester( // Build instruction let instruction = solana_instruction::Instruction { - program_id: Pubkey::from(CTOKEN_PROGRAM_ID), + program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), accounts: account_metas, data, }; diff --git a/sdk-libs/program-test/src/program_test/light_program_test.rs b/sdk-libs/program-test/src/program_test/light_program_test.rs index 9e16afd5a3..e4f60e8b0f 100644 --- a/sdk-libs/program-test/src/program_test/light_program_test.rs +++ b/sdk-libs/program-test/src/program_test/light_program_test.rs @@ -463,16 +463,14 @@ impl LightProgramTest { mint: &solana_sdk::pubkey::Pubkey, owner: &solana_sdk::pubkey::Pubkey, ) -> Result { - use light_client::indexer::{ - GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, - }; + use light_client::indexer::{GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer}; use light_compressible_client::{ pack_token_data_to_spl_bytes, AtaAccountInterface, AtaDecompressionContext, }; - use light_ctoken_sdk::ctoken::derive_ctoken_ata; - use light_sdk::constants::C_TOKEN_PROGRAM_ID; + use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID; + use light_token_sdk::token::derive_token_ata; - let (ata, bump) = derive_ctoken_ata(owner, mint); + let (ata, bump) = derive_token_ata(owner, mint); // Check on-chain first if let Some(account) = self.context.get_account(&ata) { @@ -485,9 +483,9 @@ impl LightProgramTest { } // Check compressed state - let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new(Some( - *mint, - ))); + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new( + Some(*mint), + )); let result = self .get_compressed_token_accounts_by_owner(&ata, options, None) .await?; @@ -501,7 +499,7 @@ impl LightProgramTest { let account = solana_sdk::account::Account { lamports: 0, // Compressed accounts don't have lamports data, - owner: C_TOKEN_PROGRAM_ID.into(), + owner: LIGHT_TOKEN_PROGRAM_ID.into(), executable: false, rent_epoch: 0, }; @@ -524,7 +522,7 @@ impl LightProgramTest { let account = solana_sdk::account::Account { lamports: 0, data, - owner: C_TOKEN_PROGRAM_ID.into(), + owner: LIGHT_TOKEN_PROGRAM_ID.into(), executable: false, rent_epoch: 0, }; @@ -544,20 +542,18 @@ impl LightProgramTest { mint: &solana_sdk::pubkey::Pubkey, owner: &solana_sdk::pubkey::Pubkey, ) -> Result { - use light_client::indexer::{ - GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, - }; + use light_client::indexer::{GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer}; use light_compressible_client::{AtaInterface, DecompressionContext, TokenData}; - use light_ctoken_sdk::{compat::AccountState, ctoken::derive_ctoken_ata}; + use light_token_sdk::{compat::AccountState, token::derive_token_ata}; - let (ata, bump) = derive_ctoken_ata(owner, mint); + let (ata, bump) = derive_token_ata(owner, mint); // Check on-chain first if let Some(account) = self.context.get_account(&ata) { use solana_sdk::program_pack::Pack; let token_data = if account.data.len() >= 165 { - let spl_account = - spl_token_2022::state::Account::unpack(&account.data[..165]).unwrap_or_default(); + let spl_account = spl_token_2022::state::Account::unpack(&account.data[..165]) + .unwrap_or_default(); TokenData { mint: spl_account.mint, owner: spl_account.owner, @@ -590,9 +586,9 @@ impl LightProgramTest { } // Check compressed state - let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new(Some( - *mint, - ))); + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new( + Some(*mint), + )); let result = self .get_compressed_token_accounts_by_owner(&ata, options, None) .await?; @@ -651,12 +647,13 @@ impl LightProgramTest { use borsh::BorshDeserialize; use light_client::indexer::Indexer; use light_compressible_client::{MintInterface, MintState}; - use light_ctoken_interface::{state::CompressedMint, CMINT_ADDRESS_TREE}; - use light_ctoken_sdk::ctoken::{derive_cmint_compressed_address, find_cmint_address}; + use light_token_interface::{state::CompressedMint, CMINT_ADDRESS_TREE}; + use light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address; + use light_token_sdk::token::find_mint_address; - let (cmint, _) = find_cmint_address(signer); + let (cmint, _) = find_mint_address(signer); let address_tree = solana_sdk::pubkey::Pubkey::new_from_array(CMINT_ADDRESS_TREE); - let compressed_address = derive_cmint_compressed_address(signer, &address_tree); + let compressed_address = derive_mint_compressed_address(signer, &address_tree); // Check on-chain first if let Some(account) = self.context.get_account(&cmint) { diff --git a/sdk-libs/sdk/src/compressible/decompress_runtime.rs b/sdk-libs/sdk/src/compressible/decompress_runtime.rs index 8fe36226fc..ee4382949d 100644 --- a/sdk-libs/sdk/src/compressible/decompress_runtime.rs +++ b/sdk-libs/sdk/src/compressible/decompress_runtime.rs @@ -28,12 +28,15 @@ pub trait HasTokenVariant { /// /// After Phase 8 refactor: The variant itself contains resolved seed pubkeys, /// so no accounts struct is needed for seed derivation. -pub trait CTokenSeedProvider: Copy { +pub trait TokenSeedProvider: Copy { /// Get seeds for the token account PDA (used for decompression). fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; /// Get authority seeds for signing during compression. - fn get_authority_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; + fn get_authority_seeds( + &self, + program_id: &Pubkey, + ) -> Result<(Vec>, Pubkey), ProgramError>; } /// Context trait for decompression. @@ -306,8 +309,8 @@ where .get(post_system_offset..) .ok_or_else(|| ProgramError::from(crate::error::LightSdkError::ConstraintViolation))?; - let ctoken_program = ctx - .ctoken_program() + let light_token_program = ctx + .token_program() .ok_or(ProgramError::NotEnoughAccountKeys)?; let token_rent_sponsor = ctx .token_rent_sponsor() @@ -322,7 +325,7 @@ where ctx.process_tokens( remaining_accounts, fee_payer, - token_program, + light_token_program, token_rent_sponsor, token_cpi_authority, token_config, diff --git a/sdk-libs/sdk/src/compressible/traits.rs b/sdk-libs/sdk/src/compressible/traits.rs index 901a28de41..093658b6a4 100644 --- a/sdk-libs/sdk/src/compressible/traits.rs +++ b/sdk-libs/sdk/src/compressible/traits.rs @@ -36,12 +36,12 @@ pub trait IntoVariant { /// Trait for CToken account variant types that can construct a full variant with token data. /// -/// Implemented by generated `CTokenAccountVariant` enum. +/// Implemented by generated `TokenAccountVariant` enum. /// The macro generates the impl that wraps variant + token_data into `RentFreeAccountVariant`. /// /// # Example (generated code) /// ```ignore -/// impl IntoCTokenVariant for CTokenAccountVariant { +/// impl IntoCTokenVariant for TokenAccountVariant { /// fn into_ctoken_variant(self, token_data: TokenData) -> RentFreeAccountVariant { /// RentFreeAccountVariant::CTokenData(CTokenData { /// variant: self, @@ -51,7 +51,7 @@ pub trait IntoVariant { /// } /// ``` /// -/// Type parameter `T` is typically `light_ctoken_sdk::compat::TokenData`. +/// Type parameter `T` is typically `light_token_sdk::compat::TokenData`. pub trait IntoCTokenVariant { /// Construct variant from CToken variant and token data. /// diff --git a/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs b/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs index f9916ffdd6..5ef64ec60e 100644 --- a/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs +++ b/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs @@ -12,32 +12,8 @@ use solana_pubkey::Pubkey; use crate::compat::PackedCTokenData; use crate::pack::Unpack; -<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs -/// Trait for getting token account seeds. -pub trait TokenSeedProvider: Copy { - /// Type of accounts struct needed for seed derivation. - type Accounts<'info>; - - /// Get seeds for the token account PDA (used for decompression). - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [AccountInfo<'info>], - ) -> Result<(Vec>, Pubkey), ProgramError>; - - /// Get authority seeds for signing during compression. - /// - /// TODO: consider removing. - fn get_authority_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [AccountInfo<'info>], - ) -> Result<(Vec>, Pubkey), ProgramError>; -} -======= -// Re-export CTokenSeedProvider from sdk (canonical definition). -pub use light_sdk::compressible::CTokenSeedProvider; ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs +// Re-export TokenSeedProvider from sdk (canonical definition). +pub use light_sdk::compressible::TokenSeedProvider; /// Token decompression processor. /// @@ -49,8 +25,8 @@ pub use light_sdk::compressible::CTokenSeedProvider; /// - has_prior_context=true: PDAs/Mints already wrote to CPI context, tokens CONSUME it /// - has_prior_context=false: tokens-only flow, no CPI context needed /// -/// After Phase 8 refactor: V is `PackedCTokenAccountVariant` which unpacks to -/// `CTokenAccountVariant` containing resolved seed Pubkeys. No accounts struct needed. +/// After Phase 8 refactor: V is `PackedTokenAccountVariant` which unpacks to +/// `TokenAccountVariant` containing resolved seed Pubkeys. No accounts struct needed. #[inline(never)] #[allow(clippy::too_many_arguments)] pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V>( @@ -72,29 +48,19 @@ pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V>( program_id: &Pubkey, ) -> Result<(), ProgramError> where -<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs - V: TokenSeedProvider = A>, - A: 'info, -======= V: Unpack + Copy, - V::Unpacked: CTokenSeedProvider, ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs + V::Unpacked: TokenSeedProvider, { - if ctoken_accounts.is_empty() { + if token_accounts.is_empty() { return Ok(()); } let mut token_decompress_indices: Vec< crate::compressed_token::decompress_full::DecompressFullIndices, -<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs > = Vec::with_capacity(token_accounts.len()); - let mut token_signers_seed_groups: Vec>> = Vec::with_capacity(token_accounts.len()); -======= - > = Vec::with_capacity(ctoken_accounts.len()); // Only program-owned tokens need signer seeds let mut token_signers_seed_groups: Vec>> = - Vec::with_capacity(ctoken_accounts.len()); ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs + Vec::with_capacity(token_accounts.len()); let packed_accounts = post_system_accounts; // CPI context usage for token decompression: @@ -138,19 +104,12 @@ where } let owner_info = &packed_accounts[owner_index_usize]; -<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs - // Use trait method to get seeds (program-specific) - let (token_signer_seeds, derived_token_account_address) = token_data - .variant - .get_seeds(accounts_for_seeds, remaining_accounts)?; -======= // Unpack the variant to get resolved seed Pubkeys let unpacked_variant = token_data.variant.unpack(post_system_accounts)?; // Program-owned token: use program-derived seeds let (ctoken_signer_seeds, derived_token_account_address) = unpacked_variant.get_seeds(program_id)?; ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs if derived_token_account_address != *owner_info.key { msg!( @@ -161,13 +120,6 @@ where return Err(ProgramError::InvalidAccountData); } -<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs - let seed_refs: Vec<&[u8]> = token_signer_seeds.iter().map(|s| s.as_slice()).collect(); - let seeds_slice: &[&[u8]] = &seed_refs; - - // Build CompressToPubkey from the signer seeds if bump is present - let compress_to_pubkey = token_signer_seeds -======= // Derive the authority PDA that will own this CToken account (like cp-swap's vault_authority) let (_authority_seeds, derived_authority_pda) = unpacked_variant.get_authority_seeds(program_id)?; @@ -178,13 +130,12 @@ where // Build CompressToPubkey from the token account seeds // This ensures compressed TokenData.owner = token account address (not authority) let compress_to_pubkey = ctoken_signer_seeds ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs .last() .and_then(|b| b.first().copied()) .map(|bump| { - let seeds_without_bump: Vec> = token_signer_seeds + let seeds_without_bump: Vec> = ctoken_signer_seeds .iter() - .take(token_signer_seeds.len().saturating_sub(1)) + .take(ctoken_signer_seeds.len().saturating_sub(1)) .cloned() .collect(); CompressToPubkey { @@ -198,19 +149,12 @@ where payer: fee_payer.clone(), account: (*owner_info).clone(), mint: (*mint_info).clone(), -<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs - owner: *authority.key, - compressible: crate::token::CompressibleParamsCpi { - compressible_config: token_config.clone(), - rent_sponsor: token_rent_sponsor.clone(), -======= owner: derived_authority_pda, // Use derived authority PDA (like cp-swap's vault_authority) } .invoke_signed_with( - crate::ctoken::CompressibleParamsCpi { - compressible_config: ctoken_config.clone(), - rent_sponsor: ctoken_rent_sponsor.clone(), ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs + crate::token::CompressibleParamsCpi { + compressible_config: token_config.clone(), + rent_sponsor: token_rent_sponsor.clone(), system_program: cpi_accounts .system_program() .map_err(|_| ProgramError::InvalidAccountData)? @@ -241,20 +185,15 @@ where is_ata: false, // Program-owned token: owner is a signer (via CPI seeds) }; token_decompress_indices.push(decompress_index); - token_signers_seed_groups.push(token_signer_seeds); + token_signers_seed_groups.push(ctoken_signer_seeds); } -<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs - let token_ix = - crate::compressed_token::decompress_full::decompress_full_token_accounts_with_indices( -======= if token_decompress_indices.is_empty() { return Ok(()); } let ctoken_ix = - crate::compressed_token::decompress_full::decompress_full_ctoken_accounts_with_indices( ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs + crate::compressed_token::decompress_full::decompress_full_token_accounts_with_indices( *fee_payer.key, proof, cpi_context_pubkey, @@ -329,20 +268,12 @@ where let signer_seed_slices: Vec<&[&[u8]]> = signer_seed_refs.iter().map(|g| g.as_slice()).collect(); -<<<<<<< HEAD:sdk-libs/token-sdk/src/compressible/decompress_runtime.rs - solana_cpi::invoke_signed( - &token_ix, - all_account_infos.as_slice(), - signer_seed_slices.as_slice(), - )?; -======= solana_cpi::invoke_signed( &ctoken_ix, all_account_infos.as_slice(), signer_seed_slices.as_slice(), )?; } ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs Ok(()) } diff --git a/sdk-libs/token-sdk/src/pack.rs b/sdk-libs/token-sdk/src/pack.rs index bd79805e5d..277158035e 100644 --- a/sdk-libs/token-sdk/src/pack.rs +++ b/sdk-libs/token-sdk/src/pack.rs @@ -1,9 +1,5 @@ //! Pack implementation for TokenData types for c-tokens. use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; -<<<<<<< HEAD:sdk-libs/token-sdk/src/pack.rs -======= -use light_ctoken_interface::state::TokenDataVersion; ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/pack.rs use light_sdk::{ instruction::PackedAccounts, light_hasher::{sha256::Sha256BE, HasherError}, @@ -30,39 +26,6 @@ pub trait Unpack { ) -> std::result::Result; } -<<<<<<< HEAD:sdk-libs/token-sdk/src/pack.rs -impl Pack for TokenData { - type Packed = light_token_interface::instructions::transfer2::MultiTokenTransferOutputData; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - Self::Packed { - owner: remaining_accounts.insert_or_get(self.owner.to_bytes().into()), - mint: remaining_accounts.insert_or_get_read_only(self.mint.to_bytes().into()), - amount: self.amount, - has_delegate: self.delegate.is_some(), - delegate: if let Some(delegate) = self.delegate { - remaining_accounts.insert_or_get(delegate.to_bytes().into()) - } else { - 0 - }, - version: TokenDataVersion::ShaFlat as u8, - } - } -} - -impl Unpack for TokenData { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) - } -} - -======= ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/pack.rs /// Solana-compatible token types using `solana_pubkey::Pubkey` pub mod compat { use solana_pubkey::Pubkey; @@ -153,7 +116,7 @@ pub mod compat { } } - impl From for light_ctoken_interface::state::TokenData { + impl From for light_token_interface::state::TokenData { fn from(data: TokenData) -> Self { use light_token_interface::state::CompressedTokenAccountState; @@ -171,8 +134,8 @@ pub mod compat { } } - impl From for TokenData { - fn from(data: light_ctoken_interface::state::TokenData) -> Self { + impl From for TokenData { + fn from(data: light_token_interface::state::TokenData) -> Self { Self { mint: Pubkey::new_from_array(data.mint.to_bytes()), owner: Pubkey::new_from_array(data.owner.to_bytes()), diff --git a/sdk-libs/token-sdk/src/token/create.rs b/sdk-libs/token-sdk/src/token/create.rs index 14dcbbbe12..b41f716138 100644 --- a/sdk-libs/token-sdk/src/token/create.rs +++ b/sdk-libs/token-sdk/src/token/create.rs @@ -1,6 +1,6 @@ use borsh::BorshSerialize; -use light_ctoken_interface::instructions::{ - create_ctoken_account::CreateTokenAccountInstructionData, +use light_token_interface::instructions::{ + create_token_account::CreateTokenAccountInstructionData, extensions::{CompressToPubkey, CompressibleExtensionInstructionData}, }; use solana_account_info::AccountInfo; @@ -88,7 +88,7 @@ impl CreateTokenAccount { /// /// # Example - Rent-free vault with PDA signing /// ```rust,ignore -/// CreateCTokenAccountCpi { +/// CreateTokenAccountCpi { /// payer: ctx.accounts.payer.to_account_info(), /// account: ctx.accounts.vault.to_account_info(), /// mint: ctx.accounts.mint.to_account_info(), @@ -109,7 +109,7 @@ pub struct CreateTokenAccountCpi<'info> { pub owner: Pubkey, } -impl<'info> CreateCTokenAccountCpi<'info> { +impl<'info> CreateTokenAccountCpi<'info> { /// Enable rent-free mode with compressible config. /// /// Returns a builder that can call `.invoke()` or `.invoke_signed(seeds)`. @@ -121,8 +121,8 @@ impl<'info> CreateCTokenAccountCpi<'info> { sponsor: AccountInfo<'info>, system_program: AccountInfo<'info>, program_id: &Pubkey, - ) -> CreateCTokenAccountRentFreeCpi<'info> { - CreateCTokenAccountRentFreeCpi { + ) -> CreateTokenAccountRentFreeCpi<'info> { + CreateTokenAccountRentFreeCpi { base: self, config, sponsor, @@ -136,7 +136,7 @@ impl<'info> CreateCTokenAccountCpi<'info> { self, compressible: CompressibleParamsCpi<'info>, ) -> Result<(), ProgramError> { - LegacyCreateCTokenAccountCpi { + LegacyCreateTokenAccountCpi { payer: self.payer, account: self.account, mint: self.mint, @@ -152,7 +152,7 @@ impl<'info> CreateCTokenAccountCpi<'info> { compressible: CompressibleParamsCpi<'info>, signer_seeds: &[&[&[u8]]], ) -> Result<(), ProgramError> { - LegacyCreateCTokenAccountCpi { + LegacyCreateTokenAccountCpi { payer: self.payer, account: self.account, mint: self.mint, @@ -164,20 +164,20 @@ impl<'info> CreateCTokenAccountCpi<'info> { } /// Rent-free enabled CToken account creation CPI. -pub struct CreateCTokenAccountRentFreeCpi<'info> { - base: CreateCTokenAccountCpi<'info>, +pub struct CreateTokenAccountRentFreeCpi<'info> { + base: CreateTokenAccountCpi<'info>, config: AccountInfo<'info>, sponsor: AccountInfo<'info>, system_program: AccountInfo<'info>, program_id: Pubkey, } -impl<'info> CreateCTokenAccountRentFreeCpi<'info> { +impl<'info> CreateTokenAccountRentFreeCpi<'info> { /// Invoke CPI for non-program-owned accounts. pub fn invoke(self) -> Result<(), ProgramError> { let defaults = CompressibleParams::default(); - let cpi = LegacyCreateCTokenAccountCpi { + let cpi = LegacyCreateTokenAccountCpi { payer: self.base.payer, account: self.base.account, mint: self.base.mint, @@ -217,7 +217,7 @@ impl<'info> CreateCTokenAccountRentFreeCpi<'info> { seeds: seed_vecs, }; - let cpi = LegacyCreateCTokenAccountCpi { + let cpi = LegacyCreateTokenAccountCpi { payer: self.base.payer, account: self.base.account, mint: self.base.mint, @@ -238,7 +238,7 @@ impl<'info> CreateCTokenAccountRentFreeCpi<'info> { } /// Internal legacy CPI struct with full compressible params. -struct LegacyCreateCTokenAccountCpi<'info> { +struct LegacyCreateTokenAccountCpi<'info> { payer: AccountInfo<'info>, account: AccountInfo<'info>, mint: AccountInfo<'info>, @@ -246,9 +246,9 @@ struct LegacyCreateCTokenAccountCpi<'info> { compressible: CompressibleParamsCpi<'info>, } -impl<'info> LegacyCreateCTokenAccountCpi<'info> { +impl<'info> LegacyCreateTokenAccountCpi<'info> { fn instruction(&self) -> Result { - CreateCTokenAccount { + CreateTokenAccount { payer: *self.payer.key, account: *self.account.key, mint: *self.mint.key, diff --git a/sdk-libs/token-sdk/src/token/create_ata.rs b/sdk-libs/token-sdk/src/token/create_ata.rs index 4a2330f29f..b2b983f87f 100644 --- a/sdk-libs/token-sdk/src/token/create_ata.rs +++ b/sdk-libs/token-sdk/src/token/create_ata.rs @@ -318,7 +318,7 @@ struct InternalCreateAtaCpi<'info> { impl<'info> InternalCreateAtaCpi<'info> { fn instruction(&self) -> Result { - CreateAssociatedCTokenAccount { + CreateAssociatedTokenAccount { payer: *self.payer.key, owner: *self.owner.key, mint: *self.mint.key, diff --git a/sdk-libs/token-sdk/src/token/decompress_mint.rs b/sdk-libs/token-sdk/src/token/decompress_mint.rs index dfd603d431..bb48714d31 100644 --- a/sdk-libs/token-sdk/src/token/decompress_mint.rs +++ b/sdk-libs/token-sdk/src/token/decompress_mint.rs @@ -1,14 +1,9 @@ use light_compressed_account::instruction_data::{ compressed_proof::ValidityProof, traits::LightInstructionData, }; -<<<<<<< HEAD:sdk-libs/token-sdk/src/token/decompress_mint.rs use light_token_interface::instructions::mint_action::{ - CompressedMintWithContext, DecompressMintAction, MintActionCompressedInstructionData, -======= -use light_ctoken_interface::instructions::mint_action::{ CompressedMintWithContext, CpiContext, DecompressMintAction, MintActionCompressedInstructionData, ->>>>>>> a606eb113 (wip):sdk-libs/ctoken-sdk/src/ctoken/decompress_cmint.rs }; use solana_account_info::AccountInfo; use solana_cpi::{invoke, invoke_signed}; @@ -275,11 +270,10 @@ pub struct DecompressCMintWithCpiContext { impl DecompressCMintWithCpiContext { pub fn instruction(self) -> Result { // Derive CMint PDA - let (cmint_pda, cmint_bump) = find_cmint_address(&self.mint_seed_pubkey); + let (cmint_pda, _cmint_bump) = crate::token::find_mint_address(&self.mint_seed_pubkey); // Build DecompressMintAction let action = DecompressMintAction { - cmint_bump, rent_payment: self.rent_payment, write_top_up: self.write_top_up, }; @@ -301,8 +295,7 @@ impl DecompressCMintWithCpiContext { self.input_queue, self.output_queue, ) - .with_compressible_cmint(cmint_pda, self.compressible_config, self.rent_sponsor) - .with_mint_signer_no_sign(self.mint_seed_pubkey); + .with_compressible_mint(cmint_pda, self.compressible_config, self.rent_sponsor); meta_config.cpi_context = Some(self.cpi_context_pubkey); @@ -313,7 +306,7 @@ impl DecompressCMintWithCpiContext { .map_err(|e| ProgramError::BorshIoError(e.to_string()))?; Ok(Instruction { - program_id: Pubkey::new_from_array(light_ctoken_interface::CTOKEN_PROGRAM_ID), + program_id: Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), accounts: account_metas, data, }) diff --git a/sdk-libs/token-sdk/src/token/mod.rs b/sdk-libs/token-sdk/src/token/mod.rs index 96add7a137..3cca9ce39e 100644 --- a/sdk-libs/token-sdk/src/token/mod.rs +++ b/sdk-libs/token-sdk/src/token/mod.rs @@ -6,7 +6,7 @@ //! - [`CreateAssociatedCTokenAccount`] - Create associated ctoken account (ATA) instruction //! - [`CreateCTokenAtaCpi`] - Create associated ctoken account (ATA) via CPI //! - [`CreateCTokenAccount`] - Create ctoken account instruction -//! - [`CreateCTokenAccountCpi`] - Create ctoken account via CPI +//! - [`CreateTokenAccountCpi`] - Create ctoken account via CPI //! //! ## Transfers //! @@ -52,7 +52,7 @@ //! # Example: Create rent-free ATA via CPI //! //! ```rust,ignore -//! use light_ctoken_sdk::ctoken::CreateCTokenAtaCpi; +//! use light_token_sdk::token::CreateCTokenAtaCpi; //! //! CreateCTokenAtaCpi { //! payer: ctx.accounts.payer.to_account_info(), @@ -73,9 +73,9 @@ //! # Example: Create rent-free vault via CPI (with PDA signing) //! //! ```rust,ignore -//! use light_ctoken_sdk::ctoken::CreateCTokenAccountCpi; +//! use light_token_sdk::token::CreateTokenAccountCpi; //! -//! CreateCTokenAccountCpi { +//! CreateTokenAccountCpi { //! payer: ctx.accounts.payer.to_account_info(), //! account: ctx.accounts.vault.to_account_info(), //! mint: ctx.accounts.mint.to_account_info(), @@ -120,7 +120,8 @@ pub use burn_checked::*; pub use close::{CloseAccount, CloseAccountCpi}; pub use compressible::{CompressibleParams, CompressibleParamsCpi}; pub use create::*; -pub use create_ata::{derive_token_ata, CreateAssociatedAccountCpi, CreateAssociatedTokenAccount}; +pub use create_ata::CreateCTokenAtaCpi as CreateAssociatedAccountCpi; +pub use create_ata::{derive_token_ata, CreateAssociatedTokenAccount, CreateCTokenAtaCpi}; pub use create_mint::*; pub use decompress::Decompress; pub use decompress_mint::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md b/sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md index 24eff7c227..70f40852ba 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md +++ b/sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md @@ -9,10 +9,12 @@ This test program demonstrates how `#[compressible]` PDAs and `#[light_mint]` ca ### 1. Macro Attributes **`#[compressible]`** - Applied to PDA account fields: + - `address_tree_info`: Packed address tree info from params - `output_tree`: State tree index for compressed account output **`#[light_mint]`** - Applied to mint placeholder fields: + - `mint_signer`: PDA that derives the CMint address - `authority`: Mint authority (must be signer) - `decimals`: Mint decimals @@ -22,10 +24,12 @@ This test program demonstrates how `#[compressible]` PDAs and `#[light_mint]` ca ### 2. Derive Macros **`#[derive(LightFinalize)]`** - Implements `LightPreInit` and `LightFinalize` traits: + - Detects `#[compressible]` and `#[light_mint]` fields -- Auto-detects ctoken accounts: `ctoken_compressible_config`, `ctoken_rent_sponsor`, `ctoken_program`, `ctoken_cpi_authority` +- Auto-detects ctoken accounts: `ctoken_compressible_config`, `ctoken_rent_sponsor`, `light_token_program`, `ctoken_cpi_authority` **`#[light_instruction(params)]`** - Wraps instruction handlers: + - Calls `light_pre_init()` BEFORE instruction body (all compression logic here) - Calls `light_finalize()` AFTER instruction body (no-op) @@ -72,23 +76,27 @@ Anchor Exit (serializes all account data) ### 4. Key Design Decisions -**All compression in pre_init**: +**All compression in pre_init**: + - CMint is created and decompressed BEFORE instruction body runs - Instruction body can immediately use the HOT mint (mintTo, burn, etc.) - This enables patterns like `raydium-cp-swap` where mint operations follow creation **with_data=false for PDAs**: + - Compressed account only gets the address (no data hash) - Actual data stays on-chain PDA with CompressionInfo - Later auto-compression will fully compress and close the PDA - SDK enforces this: `with_data=true` throws "not supported yet" **CPI Context Batching**: When PDAs and mints are combined: + 1. PDAs are written to CPI context first via `write_to_cpi_context_first()` 2. Mint action reads from the same CPI context (set_context: false) 3. Light System processes all operations atomically **Tree Indexing**: Critical for CPI context validation: + - `in_tree_index` is 1-indexed (Light System does `in_tree_index - 1`) - Points to the state queue, which has `associated_merkle_tree` - Must match the CPI context's `associated_merkle_tree` @@ -101,24 +109,24 @@ pub struct CreatePdasAndMintAuto<'info> { pub authority: Signer<'info>, pub mint_authority: Signer<'info>, pub mint_signer: UncheckedAccount<'info>, // CMint derives from this - + #[compressible(...)] pub user_record: Account<'info, UserRecord>, // PDA to compress - + #[compressible(...)] pub game_session: Account<'info, GameSession>, // Another PDA - + #[light_mint(...)] pub lp_mint: UncheckedAccount<'info>, // CMint placeholder (HOT after pre_init) - + pub vault: UncheckedAccount<'info>, // Program-owned CToken vault pub vault_authority: UncheckedAccount<'info>, // Vault owner PDA pub user_ata: UncheckedAccount<'info>, // User's ATA for lp_mint - + pub compression_config: AccountInfo<'info>, // Light protocol config pub ctoken_compressible_config: AccountInfo<'info>, // Ctoken config pub ctoken_rent_sponsor: AccountInfo<'info>, // Rent sponsor - pub ctoken_program: AccountInfo<'info>, // Ctoken program + pub light_token_program: AccountInfo<'info>, // Ctoken program pub ctoken_cpi_authority: AccountInfo<'info>, // Ctoken CPI authority pub system_program: Program<'info, System>, } @@ -134,16 +142,16 @@ pub fn create_pdas_and_mint_auto<'info>(ctx: ..., params: ...) -> Result<()> { // 1. Populate PDA data (compression handled by macro) ctx.accounts.user_record.owner = params.owner; ctx.accounts.game_session.session_id = params.session_id; - + // 2. Create program-owned CToken vault (like cp-swap's token vaults) - CreateCTokenAccountCpi { + CreateTokenAccountCpi { payer: ctx.accounts.fee_payer.to_account_info(), account: ctx.accounts.vault.to_account_info(), mint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint from pre_init owner: ctx.accounts.vault_authority.key(), compressible: CompressibleParamsCpi { ... }, }.invoke_signed(&[vault_seeds])?; - + // 3. Create user's ATA (like cp-swap's creator_lp_token) CreateAssociatedCTokenAccountCpi { owner: ctx.accounts.fee_payer.to_account_info(), @@ -151,7 +159,7 @@ pub fn create_pdas_and_mint_auto<'info>(ctx: ..., params: ...) -> Result<()> { associated_token_account: ctx.accounts.user_ata.to_account_info(), compressible: CompressibleParamsCpi { ... }, }.invoke()?; - + // 4. Mint tokens to vault and user's ATA CTokenMintToCpi { cmint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint @@ -159,14 +167,14 @@ pub fn create_pdas_and_mint_auto<'info>(ctx: ..., params: ...) -> Result<()> { amount: params.vault_mint_amount, authority: ctx.accounts.mint_authority.to_account_info(), }.invoke()?; - + CTokenMintToCpi { cmint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint destination: ctx.accounts.user_ata.to_account_info(), amount: params.user_ata_mint_amount, authority: ctx.accounts.mint_authority.to_account_info(), }.invoke()?; - + Ok(()) } ``` @@ -174,6 +182,7 @@ pub fn create_pdas_and_mint_auto<'info>(ctx: ..., params: ...) -> Result<()> { ### 7. Test: `test_create_pdas_and_mint_auto` Demonstrates the full cp-swap-like flow: + 1. Setup compression config and signers 2. Derive PDAs, CMint, vault, and user_ata addresses 3. Get validity proof for all 3 compressed addresses (2 PDAs + 1 mint) @@ -191,6 +200,7 @@ Demonstrates the full cp-swap-like flow: The macro system enables atomic creation of an arbitrary combination of compressed PDAs and decompressed mints in a single instruction with a single proof. All compression logic runs in `light_pre_init()`, so the instruction body can immediately use the HOT CMint for operations like `mintTo`, `burn`, and `transfer`. This pattern is essential for programs like `raydium-cp-swap` where multiple accounts (pool state, observation state, LP mint, token vaults, user ATAs) must be created and operated on atomically. **The full flow in one instruction:** + 1. `pre_init()`: Compress 2 PDAs + Create+Decompress CMint (atomically) 2. `instruction body`: Create vault + Create user_ata + MintTo both 3. `finalize()`: no-op 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 d7d72b0aa2..2b806739de 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 @@ -85,7 +85,7 @@ pub struct CreatePdasAndMintAuto<'info> { seeds = [VAULT_SEED, cmint.key().as_ref()], bump, )] - #[rentfree_token(Vault, authority = [b"vault_authority"])] + #[rentfree_token(authority = [b"vault_authority"])] pub vault: UncheckedAccount<'info>, /// CHECK: PDA used as vault owner @@ -107,7 +107,7 @@ pub struct CreatePdasAndMintAuto<'info> { pub ctoken_rent_sponsor: AccountInfo<'info>, /// CHECK: CToken program - pub ctoken_program: AccountInfo<'info>, + pub light_token_program: AccountInfo<'info>, /// CHECK: CToken CPI authority pub ctoken_cpi_authority: AccountInfo<'info>, 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 bac069bfb9..445ebf724c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -54,8 +54,8 @@ pub mod csdk_anchor_full_derived_test { params: FullAutoWithMintParams, ) -> Result<()> { use anchor_lang::solana_program::sysvar::clock::Clock; - use light_ctoken_sdk::ctoken::{ - CTokenMintToCpi, CreateCTokenAccountCpi, CreateCTokenAtaCpi, + use light_token_sdk::token::{ + CreateCTokenAtaCpi, CreateTokenAccountCpi, MintToCpi as CTokenMintToCpi, }; let user_record = &mut ctx.accounts.user_record; @@ -73,7 +73,7 @@ pub mod csdk_anchor_full_derived_test { game_session.score = 0; let cmint_key = ctx.accounts.cmint.key(); - CreateCTokenAccountCpi { + CreateTokenAccountCpi { payer: ctx.accounts.fee_payer.to_account_info(), account: ctx.accounts.vault.to_account_info(), mint: ctx.accounts.cmint.to_account_info(), diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state.rs index 91595058f6..e0f50c9c23 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state.rs @@ -1,16 +1,6 @@ use anchor_lang::prelude::*; -<<<<<<< HEAD -use light_sdk::{ - compressible::CompressionInfo, - instruction::{PackedAddressTreeInfo, ValidityProof}, - LightDiscriminator, LightHasher, -}; -use light_sdk_macros::{Compressible, CompressiblePack}; -use light_token_interface::instructions::mint_action::CompressedMintWithContext; -======= use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; use light_sdk_macros::Light; ->>>>>>> a606eb113 (wip) #[derive(Default, Debug, InitSpace, Light)] #[account] 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 5da280ff24..c2114e987a 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 @@ -3,7 +3,6 @@ use light_compressible::rent::SLOTS_PER_EPOCH; use light_compressible_client::{ get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, }; -use light_ctoken_sdk::compressed_token::create_compressed_mint::find_cmint_address; use light_macros::pubkey; use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, @@ -17,6 +16,7 @@ use light_token_interface::{ use light_token_sdk::compressed_token::create_compressed_mint::{ derive_mint_compressed_address, find_mint_address, }; +use light_token_sdk::token::find_mint_address as find_cmint_address; use light_token_types::CPI_AUTHORITY_PDA; use solana_instruction::Instruction; use solana_keypair::Keypair; @@ -32,8 +32,9 @@ const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcA async fn test_create_pdas_and_mint_auto() { use csdk_anchor_full_derived_test::instruction_accounts::{LP_MINT_SIGNER_SEED, VAULT_SEED}; use csdk_anchor_full_derived_test::FullAutoWithMintParams; - use light_ctoken_sdk::ctoken::{ - get_associated_ctoken_address_and_bump, CToken, COMPRESSIBLE_CONFIG_V1, + use light_token_interface::state::Token; + use light_token_sdk::token::{ + get_associated_token_address_and_bump, COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as CTOKEN_RENT_SPONSOR, }; @@ -45,7 +46,7 @@ async fn test_create_pdas_and_mint_auto() { let acc = rpc.get_account(*pda).await.unwrap(); assert!(acc.is_none() || acc.unwrap().lamports == 0); } - fn parse_ctoken(data: &[u8]) -> CToken { + fn parse_token(data: &[u8]) -> Token { borsh::BorshDeserialize::deserialize(&mut &data[..]).unwrap() } async fn assert_compressed_exists_with_data(rpc: &mut LightProgramTest, addr: [u8; 32]) { @@ -117,7 +118,7 @@ async fn test_create_pdas_and_mint_auto() { Pubkey::find_program_address(&[VAULT_SEED, cmint_pda.as_ref()], &program_id); let (vault_authority_pda, _) = Pubkey::find_program_address(&[b"vault_authority"], &program_id); let (user_ata_pda, user_ata_bump) = - get_associated_ctoken_address_and_bump(&payer.pubkey(), &cmint_pda); + get_associated_token_address_and_bump(&payer.pubkey(), &cmint_pda); let (user_record_pda, _) = Pubkey::find_program_address( &[ @@ -166,7 +167,7 @@ async fn test_create_pdas_and_mint_auto() { &program_id.to_bytes(), ); let mint_compressed_address = - light_ctoken_sdk::compressed_token::create_compressed_mint::derive_cmint_compressed_address( + light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address( &mint_signer_pda, &address_tree_pubkey, ); @@ -185,8 +186,8 @@ async fn test_create_pdas_and_mint_auto() { compression_config: config_pda, ctoken_compressible_config: COMPRESSIBLE_CONFIG_V1, ctoken_rent_sponsor: CTOKEN_RENT_SPONSOR, - ctoken_program: C_TOKEN_PROGRAM_ID.into(), - ctoken_cpi_authority: light_ctoken_types::CPI_AUTHORITY_PDA.into(), + 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, }; @@ -231,11 +232,11 @@ async fn test_create_pdas_and_mint_auto() { assert_onchain_exists(&mut rpc, &user_ata_pda).await; // Parse and verify CToken data - let vault_data = parse_ctoken(&rpc.get_account(vault_pda).await.unwrap().unwrap().data); + let vault_data = parse_token(&rpc.get_account(vault_pda).await.unwrap().unwrap().data); assert_eq!(vault_data.owner, vault_authority_pda.to_bytes()); assert_eq!(vault_data.amount, vault_mint_amount); - let user_ata_data = parse_ctoken(&rpc.get_account(user_ata_pda).await.unwrap().unwrap().data); + let user_ata_data = parse_token(&rpc.get_account(user_ata_pda).await.unwrap().unwrap().data); assert_eq!(user_ata_data.owner, payer.pubkey().to_bytes()); assert_eq!(user_ata_data.amount, user_ata_mint_amount); @@ -270,7 +271,7 @@ async fn test_create_pdas_and_mint_auto() { // PHASE 3: Decompress PDAs + vault via build_decompress_idempotent use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ - CTokenAccountVariant, GameSessionSeeds, UserRecordSeeds, + GameSessionSeeds, TokenAccountVariant, UserRecordSeeds, }; use light_compressible_client::{ compressible_instruction, AccountInterface, RentFreeDecompressAccount, @@ -338,7 +339,7 @@ async fn test_create_pdas_and_mint_auto() { .expect("GameSession seed verification failed"), RentFreeDecompressAccount::from_ctoken( AccountInterface::cold(vault_pda, compressed_vault.account.clone()), - CTokenAccountVariant::Vault { cmint: cmint_pda }, + TokenAccountVariant::Vault { cmint: cmint_pda }, ) .expect("CToken variant construction failed"), ]; @@ -364,7 +365,7 @@ async fn test_create_pdas_and_mint_auto() { // Assert vault is back on-chain with correct balance assert_onchain_exists(&mut rpc, &vault_pda).await; - let vault_after = parse_ctoken(&rpc.get_account(vault_pda).await.unwrap().unwrap().data); + let vault_after = parse_token(&rpc.get_account(vault_pda).await.unwrap().unwrap().data); assert_eq!(vault_after.amount, vault_mint_amount); // Verify compressed vault token is consumed (no more compressed token accounts for vault) @@ -425,7 +426,7 @@ async fn test_create_pdas_and_mint_auto() { // Assert user ATA is back on-chain with correct balance assert_onchain_exists(&mut rpc, &user_ata_pda).await; - let user_ata_after = parse_ctoken(&rpc.get_account(user_ata_pda).await.unwrap().unwrap().data); + let user_ata_after = parse_token(&rpc.get_account(user_ata_pda).await.unwrap().unwrap().data); assert_eq!(user_ata_after.amount, user_ata_mint_amount); // Verify idempotency: calling again should return empty vec diff --git a/sdk-tests/sdk-light-token-test/README.md b/sdk-tests/sdk-light-token-test/README.md index 87681eb6e5..56ae03ca78 100644 --- a/sdk-tests/sdk-light-token-test/README.md +++ b/sdk-tests/sdk-light-token-test/README.md @@ -70,7 +70,7 @@ All instructions use the **builder pattern** from `light-token-sdk::ctoken`: - **create_cmint** (Instruction 0): Create compressed mint using `CreateCMintCpi::invoke()` - **mint_to_ctoken** (Instruction 1): Mint tokens to compressed accounts using `MintToCTokenCpi::invoke()` -- **create_token_account_invoke** (Instruction 2): Create compressible token account using `CreateCTokenAccountCpi` +- **create_token_account_invoke** (Instruction 2): Create compressible token account using `CreateTokenAccountCpi` - **create_token_account_invoke_signed** (Instruction 3): Create with PDA ownership using `invoke_signed()` - **create_ata_invoke** (Instruction 4): Create compressible ATA using `CreateAssociatedTokenAccountCpi` - **create_ata_invoke_signed** (Instruction 5): Create ATA with PDA ownership using `invoke_signed()` @@ -122,6 +122,7 @@ cargo test-sbf ### Compressible Token Accounts Compressible token accounts have a special extension that allows them to be: + - Compressed back into compressed state - Configured with rent payment mechanisms - Automatically closed and compressed @@ -129,11 +130,13 @@ Compressible token accounts have a special extension that allows them to be: ### PDA Patterns (invoke_signed) The `invoke_signed` variants demonstrate how to: + 1. Derive a PDA from the program 2. Use the PDA as the authority/owner for token accounts 3. Sign transactions on behalf of the PDA This is useful for: + - Escrow programs - Vaults - Program-controlled liquidity diff --git a/sdk-tests/sdk-light-token-test/src/approve.rs b/sdk-tests/sdk-light-token-test/src/approve.rs index d8be99d6b0..9a51f82d26 100644 --- a/sdk-tests/sdk-light-token-test/src/approve.rs +++ b/sdk-tests/sdk-light-token-test/src/approve.rs @@ -17,7 +17,7 @@ pub struct ApproveData { /// - accounts[1]: delegate /// - accounts[2]: owner (signer) /// - accounts[3]: system_program -/// - accounts[4]: ctoken_program +/// - accounts[4]: light_token_program pub fn process_approve_invoke( accounts: &[AccountInfo], data: ApproveData, @@ -45,7 +45,7 @@ pub fn process_approve_invoke( /// - accounts[1]: delegate /// - accounts[2]: PDA owner (program signs) /// - accounts[3]: system_program -/// - accounts[4]: ctoken_program +/// - accounts[4]: light_token_program pub fn process_approve_invoke_signed( accounts: &[AccountInfo], data: ApproveData, diff --git a/sdk-tests/sdk-light-token-test/src/burn.rs b/sdk-tests/sdk-light-token-test/src/burn.rs index 8e0c46767a..ca55f300b0 100644 --- a/sdk-tests/sdk-light-token-test/src/burn.rs +++ b/sdk-tests/sdk-light-token-test/src/burn.rs @@ -16,7 +16,7 @@ pub struct BurnData { /// - accounts[0]: source (Light Token account, writable) /// - accounts[1]: cmint (writable) /// - accounts[2]: authority (owner, signer) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_burn_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); @@ -40,7 +40,7 @@ pub fn process_burn_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), /// - accounts[0]: source (Light Token account, writable) /// - accounts[1]: cmint (writable) /// - accounts[2]: PDA authority (owner, program signs) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_burn_invoke_signed( accounts: &[AccountInfo], amount: u64, diff --git a/sdk-tests/sdk-light-token-test/src/create_ata.rs b/sdk-tests/sdk-light-token-test/src/create_ata.rs index a14a9a099f..05fec5045e 100644 --- a/sdk-tests/sdk-light-token-test/src/create_ata.rs +++ b/sdk-tests/sdk-light-token-test/src/create_ata.rs @@ -1,5 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use light_ctoken_sdk::ctoken::CreateCTokenAtaCpi; +use light_token_sdk::token::CreateCTokenAtaCpi; use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; use crate::{ATA_SEED, ID}; diff --git a/sdk-tests/sdk-light-token-test/src/create_token_account.rs b/sdk-tests/sdk-light-token-test/src/create_token_account.rs index 0301da922e..43c847386c 100644 --- a/sdk-tests/sdk-light-token-test/src/create_token_account.rs +++ b/sdk-tests/sdk-light-token-test/src/create_token_account.rs @@ -41,7 +41,7 @@ pub fn process_create_token_account_invoke( ); // Build the account infos struct and invoke with custom compressible params - CreateCTokenAccountCpi { + CreateTokenAccountCpi { payer: accounts[0].clone(), account: accounts[1].clone(), mint: accounts[2].clone(), @@ -86,7 +86,7 @@ pub fn process_create_token_account_invoke_signed( // Invoke with PDA signing and custom compressible params let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; - CreateCTokenAccountCpi { + CreateTokenAccountCpi { payer: accounts[0].clone(), account: accounts[1].clone(), mint: accounts[2].clone(), diff --git a/sdk-tests/sdk-light-token-test/src/freeze.rs b/sdk-tests/sdk-light-token-test/src/freeze.rs index 16183125e8..249c804e01 100644 --- a/sdk-tests/sdk-light-token-test/src/freeze.rs +++ b/sdk-tests/sdk-light-token-test/src/freeze.rs @@ -9,7 +9,7 @@ use crate::{FREEZE_AUTHORITY_SEED, ID}; /// - accounts[0]: token_account (writable) /// - accounts[1]: mint /// - accounts[2]: freeze_authority (signer) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_freeze_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); @@ -31,7 +31,7 @@ pub fn process_freeze_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro /// - accounts[0]: token_account (writable) /// - accounts[1]: mint /// - accounts[2]: PDA freeze_authority (program signs) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_freeze_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); diff --git a/sdk-tests/sdk-light-token-test/src/revoke.rs b/sdk-tests/sdk-light-token-test/src/revoke.rs index cdd84fb7ca..c3bc1d59a6 100644 --- a/sdk-tests/sdk-light-token-test/src/revoke.rs +++ b/sdk-tests/sdk-light-token-test/src/revoke.rs @@ -9,7 +9,7 @@ use crate::{ID, TOKEN_ACCOUNT_SEED}; /// - accounts[0]: token_account (writable) /// - accounts[1]: owner (signer) /// - accounts[2]: system_program -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); @@ -31,7 +31,7 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro /// - accounts[0]: token_account (writable) /// - accounts[1]: PDA owner (program signs) /// - accounts[2]: system_program -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); diff --git a/sdk-tests/sdk-light-token-test/src/thaw.rs b/sdk-tests/sdk-light-token-test/src/thaw.rs index 5cfeffbc33..8ce5a57678 100644 --- a/sdk-tests/sdk-light-token-test/src/thaw.rs +++ b/sdk-tests/sdk-light-token-test/src/thaw.rs @@ -9,7 +9,7 @@ use crate::{FREEZE_AUTHORITY_SEED, ID}; /// - accounts[0]: token_account (writable) /// - accounts[1]: mint /// - accounts[2]: freeze_authority (signer) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_thaw_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); @@ -31,7 +31,7 @@ pub fn process_thaw_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> /// - accounts[0]: token_account (writable) /// - accounts[1]: mint /// - accounts[2]: PDA freeze_authority (program signs) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_thaw_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); diff --git a/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs b/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs index 0574e4e44a..b421501d37 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs @@ -44,7 +44,7 @@ async fn test_approve_invoke() { }; approve_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ @@ -52,7 +52,7 @@ async fn test_approve_invoke() { AccountMeta::new_readonly(delegate.pubkey(), false), // delegate AccountMeta::new(payer.pubkey(), true), // owner (signer) AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -103,7 +103,7 @@ async fn test_approve_invoke_signed() { }; approve_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ @@ -111,7 +111,7 @@ async fn test_approve_invoke_signed() { AccountMeta::new_readonly(delegate.pubkey(), false), // delegate AccountMeta::new(pda_owner, false), // PDA owner (program signs) AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -156,7 +156,7 @@ async fn test_revoke_invoke() { let ata = ata_pubkeys[0]; let delegate = Keypair::new(); let approve_amount = 100u64; - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); // First approve a delegate let mut approve_instruction_data = vec![InstructionType::ApproveInvoke as u8]; @@ -174,7 +174,7 @@ async fn test_revoke_invoke() { AccountMeta::new_readonly(delegate.pubkey(), false), AccountMeta::new(payer.pubkey(), true), AccountMeta::new_readonly(Pubkey::default(), false), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: approve_instruction_data, }; @@ -201,7 +201,7 @@ async fn test_revoke_invoke() { AccountMeta::new(ata, false), // token_account AccountMeta::new(payer.pubkey(), true), // owner (signer) AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: revoke_instruction_data, }; @@ -242,7 +242,7 @@ async fn test_revoke_invoke_signed() { let ata = ata_pubkeys[0]; let delegate = Keypair::new(); let approve_amount = 100u64; - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); // First approve a delegate using invoke_signed let mut approve_instruction_data = vec![InstructionType::ApproveInvokeSigned as u8]; @@ -260,7 +260,7 @@ async fn test_revoke_invoke_signed() { AccountMeta::new_readonly(delegate.pubkey(), false), AccountMeta::new(pda_owner, false), AccountMeta::new_readonly(Pubkey::default(), false), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: approve_instruction_data, }; @@ -287,7 +287,7 @@ async fn test_revoke_invoke_signed() { AccountMeta::new(ata, false), // token_account AccountMeta::new(pda_owner, false), // PDA owner (program signs) AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: revoke_instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_burn.rs b/sdk-tests/sdk-light-token-test/tests/test_burn.rs index 052f2ce2ab..4c0df1abac 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_burn.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_burn.rs @@ -48,14 +48,14 @@ async fn test_burn_invoke() { }; burn_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ AccountMeta::new(ata, false), // source AccountMeta::new(mint_pda, false), // cmint AccountMeta::new_readonly(payer.pubkey(), true), // authority (signer) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -114,14 +114,14 @@ async fn test_burn_invoke_signed() { }; burn_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ AccountMeta::new(ata, false), // source AccountMeta::new(mint_pda, false), // cmint AccountMeta::new_readonly(pda_owner, false), // PDA authority (program signs) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_create_ata.rs b/sdk-tests/sdk-light-token-test/tests/test_create_ata.rs index 288b4d1b83..e1bf29154f 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_create_ata.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_create_ata.rs @@ -49,7 +49,7 @@ async fn test_create_ata_invoke() { let config = config_pda(); let rent_sponsor = rent_sponsor_pda(); - // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, ctoken_program + // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, light_token_program let instruction = Instruction { program_id: ID, accounts: vec![ @@ -135,7 +135,7 @@ async fn test_create_ata_invoke_signed() { let config = config_pda(); let rent_sponsor = rent_sponsor_pda(); - // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, ctoken_program + // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, light_token_program let instruction = Instruction { program_id: ID, accounts: vec![ diff --git a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs index 6f0667eaa7..3375e8d9fa 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs @@ -48,7 +48,7 @@ async fn test_ctoken_mint_to_invoke() { }; mint_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let system_program = Pubkey::default(); let instruction = Instruction { program_id: ID, @@ -57,7 +57,7 @@ async fn test_ctoken_mint_to_invoke() { AccountMeta::new(ata, false), // destination AccountMeta::new(payer.pubkey(), true), // authority (signer, writable for top-up) AccountMeta::new_readonly(system_program, false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -307,7 +307,7 @@ async fn test_ctoken_mint_to_invoke_signed() { }; mint_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let system_program = Pubkey::default(); let instruction = Instruction { program_id: ID, @@ -316,7 +316,7 @@ async fn test_ctoken_mint_to_invoke_signed() { AccountMeta::new(ata, false), // destination AccountMeta::new(pda_mint_authority, false), // PDA authority (program signs, writable for top-up) AccountMeta::new_readonly(system_program, false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs b/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs index f7284e4a38..3ef3816f08 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs @@ -51,14 +51,14 @@ async fn test_freeze_invoke() { // Build freeze instruction via wrapper program let instruction_data = vec![InstructionType::FreezeInvoke as u8]; - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(mint_pda, false), // mint AccountMeta::new_readonly(freeze_authority.pubkey(), true), // freeze_authority (signer) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -110,14 +110,14 @@ async fn test_freeze_invoke_signed() { // Build freeze instruction via wrapper program using invoke_signed let instruction_data = vec![InstructionType::FreezeInvokeSigned as u8]; - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(mint_pda, false), // mint AccountMeta::new_readonly(pda_freeze_authority, false), // PDA freeze_authority (program signs) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -146,7 +146,7 @@ async fn test_thaw_invoke() { let payer = rpc.get_payer().insecure_clone(); let freeze_authority = Keypair::new(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); // Create a compressed mint with freeze_authority and an ATA for the payer with 1000 tokens let (mint_pda, _compression_address, ata_pubkeys) = @@ -170,7 +170,7 @@ async fn test_thaw_invoke() { AccountMeta::new(ata, false), AccountMeta::new_readonly(mint_pda, false), AccountMeta::new_readonly(freeze_authority.pubkey(), true), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: freeze_instruction_data, }; @@ -200,7 +200,7 @@ async fn test_thaw_invoke() { AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(mint_pda, false), // mint AccountMeta::new_readonly(freeze_authority.pubkey(), true), // freeze_authority (signer) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: thaw_instruction_data, }; @@ -233,7 +233,7 @@ async fn test_thaw_invoke_signed() { // Derive the PDA that will be the freeze authority let (pda_freeze_authority, _bump) = Pubkey::find_program_address(&[FREEZE_AUTHORITY_SEED], &ID); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); // Create a compressed mint with PDA freeze_authority and an ATA for the payer with 1000 tokens let (mint_pda, _compression_address, ata_pubkeys) = @@ -257,7 +257,7 @@ async fn test_thaw_invoke_signed() { AccountMeta::new(ata, false), AccountMeta::new_readonly(mint_pda, false), AccountMeta::new_readonly(pda_freeze_authority, false), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: freeze_instruction_data, }; @@ -283,7 +283,7 @@ async fn test_thaw_invoke_signed() { AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(mint_pda, false), // mint AccountMeta::new_readonly(pda_freeze_authority, false), // PDA freeze_authority (program signs) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: thaw_instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs index 393b5ecd6f..fbbfedab88 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs @@ -144,7 +144,7 @@ async fn test_ctoken_transfer_checked_spl_mint() { let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; transfer_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; + let light_token_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; let instruction = Instruction { program_id: ID, accounts: vec![ @@ -152,7 +152,7 @@ async fn test_ctoken_transfer_checked_spl_mint() { AccountMeta::new_readonly(mint, false), AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, }; @@ -249,7 +249,7 @@ async fn test_ctoken_transfer_checked_t22_mint() { let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; transfer_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; + let light_token_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; let instruction = Instruction { program_id: ID, accounts: vec![ @@ -257,7 +257,7 @@ async fn test_ctoken_transfer_checked_t22_mint() { AccountMeta::new_readonly(mint, false), AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, }; @@ -310,7 +310,7 @@ async fn test_ctoken_transfer_checked_cmint() { let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; transfer_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; + let light_token_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; let instruction = Instruction { program_id: ID, accounts: vec![ @@ -318,7 +318,7 @@ async fn test_ctoken_transfer_checked_cmint() { AccountMeta::new_readonly(mint, false), AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, }; diff --git a/sdk-tests/sdk-token-test/src/ctoken_pda/mod.rs b/sdk-tests/sdk-token-test/src/ctoken_pda/mod.rs index ec86393d57..9a327540f1 100644 --- a/sdk-tests/sdk-token-test/src/ctoken_pda/mod.rs +++ b/sdk-tests/sdk-token-test/src/ctoken_pda/mod.rs @@ -11,7 +11,7 @@ pub struct CTokenPda<'info> { pub mint_authority: Signer<'info>, pub mint_seed: Signer<'info>, /// CHECK: - pub ctoken_program: UncheckedAccount<'info>, + pub light_token_program: UncheckedAccount<'info>, /// CHECK: pub ctoken_cpi_authority: UncheckedAccount<'info>, } diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs index 794a4c09c9..1e9fa397ea 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs @@ -52,7 +52,7 @@ pub fn process_mint_action<'a, 'info>( let tree_accounts = cpi_accounts.tree_accounts().unwrap(); let ctoken_accounts_vec = vec![ctx.accounts.token_account.to_account_info()]; let mint_action_accounts = MintActionCpiAccounts { - compressed_token_program: ctx.accounts.ctoken_program.as_ref(), + compressed_token_program: ctx.accounts.light_token_program.as_ref(), light_system_program: cpi_accounts.system_program().unwrap(), mint_signer: Some(ctx.accounts.mint_seed.as_ref()), authority: ctx.accounts.mint_authority.as_ref(), @@ -76,7 +76,7 @@ pub fn process_mint_action<'a, 'info>( // Get all account infos needed for the mint action let mut account_infos = cpi_accounts.to_account_infos(); account_infos.push(ctx.accounts.ctoken_cpi_authority.to_account_info()); - account_infos.push(ctx.accounts.ctoken_program.to_account_info()); + account_infos.push(ctx.accounts.light_token_program.to_account_info()); account_infos.push(ctx.accounts.mint_authority.to_account_info()); account_infos.push(ctx.accounts.mint_seed.to_account_info()); account_infos.push(ctx.accounts.payer.to_account_info()); diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs index ad861592d2..5158622533 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs @@ -16,7 +16,7 @@ pub struct PdaCToken<'info> { #[account(mut)] pub token_account: UncheckedAccount<'info>, /// CHECK: - pub ctoken_program: UncheckedAccount<'info>, + pub light_token_program: UncheckedAccount<'info>, /// CHECK: pub ctoken_cpi_authority: UncheckedAccount<'info>, } diff --git a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs index 946c72dc4d..b7e1c70b4b 100644 --- a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs +++ b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs @@ -226,7 +226,7 @@ pub async fn create_mint( payer: payer.pubkey(), mint_authority: mint_authority.pubkey(), mint_seed: mint_seed.pubkey(), - ctoken_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), + light_token_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), ctoken_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), }; diff --git a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs index d455c0b2f2..9c40aabaa0 100644 --- a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs +++ b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs @@ -297,7 +297,7 @@ pub async fn create_mint( payer: payer.pubkey(), mint_authority: mint_authority.pubkey(), mint_seed: mint_seed.pubkey(), - ctoken_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), + light_token_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), ctoken_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), token_account, }; From 6443a47d2922a15431ec57760ed3c161e6791fbd Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 16 Jan 2026 18:53:50 +0000 Subject: [PATCH 4/9] fix --- sdk-libs/compressible-client/DECOMPRESSION.md | 14 +++++++------- sdk-libs/compressible-client/decompress-mint.md | 4 ++-- .../compressible-client/src/decompress_mint.rs | 6 +++--- sdk-libs/compressible-client/src/lib.rs | 2 +- sdk-libs/macros/src/lib.rs | 8 ++++---- .../csdk-anchor-full-derived-test/src/state.rs | 8 ++++---- .../tests/basic_test.rs | 12 ++++++------ .../sdk-light-token-test/src/decompress_cmint.rs | 2 +- sdk-tests/sdk-light-token-test/src/lib.rs | 6 +++--- .../tests/test_ctoken_mint_to.rs | 10 +++++----- .../tests/test_decompress_cmint.rs | 10 +++++----- 11 files changed, 41 insertions(+), 41 deletions(-) diff --git a/sdk-libs/compressible-client/DECOMPRESSION.md b/sdk-libs/compressible-client/DECOMPRESSION.md index a167599ca7..fbc65d320a 100644 --- a/sdk-libs/compressible-client/DECOMPRESSION.md +++ b/sdk-libs/compressible-client/DECOMPRESSION.md @@ -5,7 +5,7 @@ This document describes how to decompress compressed CToken ATAs and CMints. ## Quick Start ```rust -use light_compressible_client::{decompress_atas, decompress_cmint}; +use light_compressible_client::{decompress_atas, decompress_mint}; // Decompress ATAs let atas = vec![ @@ -15,7 +15,7 @@ let instructions = decompress_atas(&atas, fee_payer, &rpc).await?; // Decompress CMint let mint = rpc.get_mint_interface(&signer).await?; -let instructions = decompress_cmint(&mint, fee_payer, &rpc).await?; +let instructions = decompress_mint(&mint, fee_payer, &rpc).await?; ``` ## Unified Token Data @@ -56,7 +56,7 @@ if ata.is_cold() { | Function | Description | | -------------------------------------------------------------- | -------------------------------------------- | -| `decompress_cmint(&MintInterface, fee_payer, &indexer)` | High-perf wrapper: pre-fetch mint, call this | +| `decompress_mint(&MintInterface, fee_payer, &indexer)` | High-perf wrapper: pre-fetch mint, call this | | `build_decompress_mint(&MintInterface, fee_payer, proof, ...)` | Sync: caller provides proof | | `decompress_mint(signer, fee_payer, &indexer)` | Simple: fetches everything | | `rpc.get_mint_interface(&signer)` | Fetch CMint state | @@ -86,7 +86,7 @@ let instructions = decompress_mint(signer, fee_payer, &rpc).await?; Pre-fetch state, then call lean wrapper. Allows batching state fetches. ```rust -use light_compressible_client::{decompress_atas, decompress_cmint}; +use light_compressible_client::{decompress_atas, decompress_mint}; // Pre-fetch ATAs (can batch with futures::join_all) let atas = vec![ @@ -104,7 +104,7 @@ let instructions = decompress_atas(&atas, fee_payer, &rpc).await?; // Same for mints let mint = rpc.get_mint_interface(&signer).await?; -let instructions = decompress_cmint(&mint, fee_payer, &rpc).await?; +let instructions = decompress_mint(&mint, fee_payer, &rpc).await?; ``` ### Pattern 3: Maximum Control (For advanced use cases) @@ -199,7 +199,7 @@ All functions are idempotent: ## Example: Full Decompression Flow ```rust -use light_compressible_client::{decompress_atas, decompress_cmint}; +use light_compressible_client::{decompress_atas, decompress_mint}; async fn decompress_all( rpc: &mut LightProgramTest, @@ -212,7 +212,7 @@ async fn decompress_all( // 1. Decompress CMint first (required for ATA decompression) let mint_interface = rpc.get_mint_interface(&signer).await?; if mint_interface.is_cold() { - let ix = decompress_cmint(&mint_interface, fee_payer, rpc).await?; + let ix = decompress_mint(&mint_interface, fee_payer, rpc).await?; if !ix.is_empty() { rpc.create_and_send_transaction(&ix, &fee_payer, &[payer]).await?; } diff --git a/sdk-libs/compressible-client/decompress-mint.md b/sdk-libs/compressible-client/decompress-mint.md index 744358fabd..877e276dca 100644 --- a/sdk-libs/compressible-client/decompress-mint.md +++ b/sdk-libs/compressible-client/decompress-mint.md @@ -16,7 +16,7 @@ SDK-only functionality to decompress compressed CMint accounts (mints that were 2. **mint_seed does NOT need to sign** - uses `with_mint_signer_no_sign()` internally. The mint_seed is only used for PDA derivation. -3. `DecompressMint` struct already exists in `ctoken-sdk/src/ctoken/decompress_cmint.rs` with a complete `instruction()` method. +3. `DecompressMint` struct already exists in `ctoken-sdk/src/ctoken/decompress_mint.rs` with a complete `instruction()` method. 4. All data needed is queryable from the indexer via the compressed mint's address. @@ -77,7 +77,7 @@ Unlike PDAs which require program signing for decompression, CMint decompression | Component | Location | Reuse | | ------------------------------------- | -------------------------------------------------------------------------- | ------ | -| `DecompressMint` struct | `ctoken-sdk/src/ctoken/decompress_cmint.rs` | Direct | +| `DecompressMint` struct | `ctoken-sdk/src/ctoken/decompress_mint.rs` | Direct | | `find_mint_address` | `ctoken-sdk/src/ctoken/create_cmint.rs` | Direct | | `derive_cmint_compressed_address` | `ctoken-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs` | Direct | | `CompressedMintWithContext` | `ctoken-interface/src/instructions/mint_action/instruction_data.rs` | Direct | diff --git a/sdk-libs/compressible-client/src/decompress_mint.rs b/sdk-libs/compressible-client/src/decompress_mint.rs index 0301260073..87dbc5fb64 100644 --- a/sdk-libs/compressible-client/src/decompress_mint.rs +++ b/sdk-libs/compressible-client/src/decompress_mint.rs @@ -10,7 +10,7 @@ //! Three APIs are provided: //! - `decompress_mint`: Simple async API (fetches state + proof internally) //! - `build_decompress_mint`: Sync, caller provides pre-fetched state + proof -//! - `decompress_cmint`: High-perf wrapper (takes MintInterface, fetches proof internally) +//! - `decompress_mint`: High-perf wrapper (takes MintInterface, fetches proof internally) use borsh::BorshDeserialize; use light_client::indexer::{CompressedAccount, Indexer, IndexerError, ValidityProofWithContext}; @@ -216,9 +216,9 @@ pub fn build_decompress_mint( /// let mint = rpc.get_mint_interface(&signer).await?; /// /// // Decompress if cold (fetches proof internally) -/// let instructions = decompress_cmint(&mint, fee_payer, &rpc).await?; +/// let instructions = decompress_mint(&mint, fee_payer, &rpc).await?; /// ``` -pub async fn decompress_cmint( +pub async fn decompress_mint( mint: &MintInterface, fee_payer: Pubkey, indexer: &I, diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs index 59831dcc8e..bc88797970 100644 --- a/sdk-libs/compressible-client/src/lib.rs +++ b/sdk-libs/compressible-client/src/lib.rs @@ -30,7 +30,7 @@ pub use decompress_atas::{ pub use light_compressible::CreateAccountsProof; // Re-export TokenData for convenience (standard SPL-compatible type) pub use decompress_mint::{ - build_decompress_mint, create_mint_interface, decompress_cmint, decompress_mint, + build_decompress_mint, create_mint_interface, decompress_mint, decompress_mint, decompress_mint_idempotent, DecompressMintError, DecompressMintRequest, MintInterface, MintState, DEFAULT_RENT_PAYMENT, DEFAULT_WRITE_TOP_UP, }; diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index e729a15344..8fc8f6fdd3 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -296,11 +296,11 @@ pub fn compressible_pack(input: TokenStream) -> TokenStream { /// ## Example /// /// ```ignore -/// use light_sdk_macros::Light; +/// use light_sdk_macros::RentFree; /// use light_sdk::compressible::CompressionInfo; /// use solana_pubkey::Pubkey; /// -/// #[derive(Default, Debug, InitSpace, Light)] +/// #[derive(Default, Debug, InitSpace, RentFree)] /// #[account] /// pub struct UserRecord { /// pub owner: Pubkey, @@ -327,8 +327,8 @@ pub fn compressible_pack(input: TokenStream) -> TokenStream { /// - The `compression_info` field is auto-detected and handled (no `#[skip]` needed) /// - SHA256 (ShaFlat) hashes the entire serialized struct (no `#[hash]` needed) /// - The struct must have a `compression_info: Option` field -#[proc_macro_derive(Light, attributes(compress_as))] -pub fn light(input: TokenStream) -> TokenStream { +#[proc_macro_derive(RentFree, attributes(compress_as))] +pub fn rent_free(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); into_token_stream(compressible::light_compressible::derive_light_compressible( input, diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state.rs index e0f50c9c23..34f696f1b1 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state.rs @@ -1,8 +1,8 @@ use anchor_lang::prelude::*; use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::Light; +use light_sdk_macros::RentFree; -#[derive(Default, Debug, InitSpace, Light)] +#[derive(Default, Debug, InitSpace, RentFree)] #[account] pub struct UserRecord { pub compression_info: Option, @@ -13,7 +13,7 @@ pub struct UserRecord { pub category_id: u64, } -#[derive(Default, Debug, InitSpace, Light)] +#[derive(Default, Debug, InitSpace, RentFree)] #[compress_as(start_time = 0, end_time = None, score = 0)] #[account] pub struct GameSession { @@ -27,7 +27,7 @@ pub struct GameSession { pub score: u64, } -#[derive(Default, Debug, InitSpace, Light)] +#[derive(Default, Debug, InitSpace, RentFree)] #[account] pub struct PlaceholderRecord { pub compression_info: Option, 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 c2114e987a..d40b4ab65c 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 @@ -379,7 +379,7 @@ async fn test_create_pdas_and_mint_auto() { // PHASE 4: Decompress user ATA via new high-performance API pattern use light_compressible_client::{ - build_decompress_token_accounts, decompress_cmint, decompress_token_accounts, + build_decompress_token_accounts, decompress_mint, decompress_token_accounts, parse_token_account_interface, }; @@ -456,7 +456,7 @@ async fn test_create_pdas_and_mint_auto() { "Should return empty vec when already decompressed" ); - // PHASE 5: Decompress CMint via decompress_cmint (lean wrapper) + // PHASE 5: Decompress CMint via decompress_mint (lean wrapper) let mint_interface = rpc .get_mint_interface(&mint_signer_pda) .await @@ -466,9 +466,9 @@ async fn test_create_pdas_and_mint_auto() { assert!(mint_interface.is_cold(), "Mint should be cold after warp"); // Decompress using lean wrapper (fetches proof internally) - let mint_instructions = decompress_cmint(&mint_interface, payer.pubkey(), &rpc) + let mint_instructions = decompress_mint(&mint_interface, payer.pubkey(), &rpc) .await - .expect("decompress_cmint should succeed"); + .expect("decompress_mint should succeed"); if !mint_instructions.is_empty() { rpc.create_and_send_transaction(&mint_instructions, &payer.pubkey(), &[&payer]) @@ -488,9 +488,9 @@ async fn test_create_pdas_and_mint_auto() { mint_interface_again.is_hot(), "Mint should be hot after decompression" ); - let mint_instructions_again = decompress_cmint(&mint_interface_again, payer.pubkey(), &rpc) + let mint_instructions_again = decompress_mint(&mint_interface_again, payer.pubkey(), &rpc) .await - .expect("decompress_cmint should succeed"); + .expect("decompress_mint should succeed"); assert!( mint_instructions_again.is_empty(), "Should return empty vec when mint already decompressed" diff --git a/sdk-tests/sdk-light-token-test/src/decompress_cmint.rs b/sdk-tests/sdk-light-token-test/src/decompress_cmint.rs index 92ac319e89..d91aa3aa58 100644 --- a/sdk-tests/sdk-light-token-test/src/decompress_cmint.rs +++ b/sdk-tests/sdk-light-token-test/src/decompress_cmint.rs @@ -34,7 +34,7 @@ pub struct DecompressCmintData { /// - accounts[12]: account_compression_authority (readonly) /// - accounts[13]: account_compression_program (readonly) /// - accounts[14]: system_program (readonly) -pub fn process_decompress_cmint_invoke_signed( +pub fn process_decompress_mint_invoke_signed( accounts: &[AccountInfo], data: DecompressCmintData, ) -> Result<(), ProgramError> { diff --git a/sdk-tests/sdk-light-token-test/src/lib.rs b/sdk-tests/sdk-light-token-test/src/lib.rs index 840805cdba..4fc437dc61 100644 --- a/sdk-tests/sdk-light-token-test/src/lib.rs +++ b/sdk-tests/sdk-light-token-test/src/lib.rs @@ -7,7 +7,7 @@ mod create_ata; mod create_cmint; mod create_token_account; mod ctoken_mint_to; -mod decompress_cmint; +mod decompress_mint; mod freeze; mod revoke; mod thaw; @@ -30,7 +30,7 @@ pub use create_token_account::{ CreateTokenAccountData, }; pub use ctoken_mint_to::{process_mint_to_invoke, process_mint_to_invoke_signed, MintToData}; -pub use decompress_cmint::{process_decompress_cmint_invoke_signed, DecompressCmintData}; +pub use decompress_mint::{process_decompress_mint_invoke_signed, DecompressCmintData}; pub use freeze::{process_freeze_invoke, process_freeze_invoke_signed}; pub use revoke::{process_revoke_invoke, process_revoke_invoke_signed}; use solana_program::{ @@ -315,7 +315,7 @@ pub fn process_instruction( InstructionType::DecompressCmintInvokeSigned => { let data = DecompressCmintData::try_from_slice(&instruction_data[1..]) .map_err(|_| ProgramError::InvalidInstructionData)?; - process_decompress_cmint_invoke_signed(accounts, data) + process_decompress_mint_invoke_signed(accounts, data) } InstructionType::CTokenTransferCheckedInvoke => { let data = TransferCheckedData::try_from_slice(&instruction_data[1..]) diff --git a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs index 3375e8d9fa..064c664197 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs @@ -53,8 +53,8 @@ async fn test_ctoken_mint_to_invoke() { let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(mint_pda, false), // cmint - AccountMeta::new(ata, false), // destination + AccountMeta::new(mint_pda, false), // cmint + AccountMeta::new(ata, false), // destination AccountMeta::new(payer.pubkey(), true), // authority (signer, writable for top-up) AccountMeta::new_readonly(system_program, false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program @@ -233,7 +233,7 @@ async fn test_ctoken_mint_to_invoke_signed() { ] .concat(); - // Account order matches process_decompress_cmint_invoke_signed: + // Account order matches process_decompress_mint_invoke_signed: // 0: authority (PDA, readonly - program signs) // 1: payer (signer, writable) // 2: cmint (writable) @@ -312,8 +312,8 @@ async fn test_ctoken_mint_to_invoke_signed() { let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(mint_pda, false), // cmint - AccountMeta::new(ata, false), // destination + AccountMeta::new(mint_pda, false), // cmint + AccountMeta::new(ata, false), // destination AccountMeta::new(pda_mint_authority, false), // PDA authority (program signs, writable for top-up) AccountMeta::new_readonly(system_program, false), // system_program AccountMeta::new_readonly(light_token_program, false), // light_token_program diff --git a/sdk-tests/sdk-light-token-test/tests/test_decompress_cmint.rs b/sdk-tests/sdk-light-token-test/tests/test_decompress_cmint.rs index a07621a232..a6bd5ef4ea 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_decompress_cmint.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_decompress_cmint.rs @@ -14,7 +14,7 @@ use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; /// Test decompressing a compressed mint to CMint account #[tokio::test] -async fn test_decompress_cmint() { +async fn test_decompress_mint() { let config = ProgramTestConfig::new_v2(true, None); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -114,7 +114,7 @@ async fn test_decompress_cmint() { /// Test decompressing a compressed mint with freeze_authority #[tokio::test] -async fn test_decompress_cmint_with_freeze_authority() { +async fn test_decompress_mint_with_freeze_authority() { let config = ProgramTestConfig::new_v2(true, None); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -299,7 +299,7 @@ async fn setup_create_compressed_mint_with_freeze_authority_only( /// Test decompressing a compressed mint with TokenMetadata extension #[tokio::test] -async fn test_decompress_cmint_with_token_metadata() { +async fn test_decompress_mint_with_token_metadata() { use light_token_interface::instructions::extensions::{ ExtensionInstructionData, TokenMetadataInstructionData, }; @@ -506,7 +506,7 @@ async fn setup_create_compressed_mint_with_extensions( /// Test decompressing a compressed mint via CPI with PDA authority using invoke_signed #[tokio::test] -async fn test_decompress_cmint_cpi_invoke_signed() { +async fn test_decompress_mint_cpi_invoke_signed() { use borsh::BorshSerialize; use native_ctoken_examples::{ CreateCmintData, DecompressCmintData, InstructionType, ID, MINT_AUTHORITY_SEED, @@ -655,7 +655,7 @@ async fn test_decompress_cmint_cpi_invoke_signed() { ] .concat(); - // Account order matches process_decompress_cmint_invoke_signed: + // Account order matches process_decompress_mint_invoke_signed: // 0: authority (PDA, readonly - program signs) // 1: payer (signer, writable) // 2: cmint (writable) From e62f7d62d50380e0d8eaa682aa0e3ed41020e75b Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 16 Jan 2026 19:13:31 +0000 Subject: [PATCH 5/9] fix --- .../macros/src/compressible/file_scanner.rs | 32 +++-- .../macros/src/compressible/instructions.rs | 111 ++++++++++++++++- sdk-libs/macros/src/finalize/instruction.rs | 115 ------------------ sdk-libs/macros/src/finalize/mod.rs | 6 +- sdk-libs/macros/src/lib.rs | 66 ++-------- .../csdk-anchor-full-derived-test/src/lib.rs | 3 +- .../src/state.rs | 8 +- 7 files changed, 151 insertions(+), 190 deletions(-) delete mode 100644 sdk-libs/macros/src/finalize/instruction.rs diff --git a/sdk-libs/macros/src/compressible/file_scanner.rs b/sdk-libs/macros/src/compressible/file_scanner.rs index 732ff11060..e9300b07ba 100644 --- a/sdk-libs/macros/src/compressible/file_scanner.rs +++ b/sdk-libs/macros/src/compressible/file_scanner.rs @@ -16,6 +16,8 @@ pub struct ScannedModuleInfo { pub pda_specs: Vec, pub token_specs: Vec, pub errors: Vec, + /// Names of Accounts structs that have rentfree fields (for auto-wrapping handlers) + pub rentfree_struct_names: std::collections::HashSet, } /// Scan the entire src/ directory for Accounts structs with #[rentfree] fields. @@ -39,7 +41,9 @@ fn scan_directory_recursive(dir: &Path, result: &mut ScannedModuleInfo) { let entries = match std::fs::read_dir(dir) { Ok(e) => e, Err(e) => { - result.errors.push(format!("Failed to read directory {:?}: {}", dir, e)); + result + .errors + .push(format!("Failed to read directory {:?}: {}", dir, e)); return; } }; @@ -60,7 +64,9 @@ fn scan_rust_file(path: &Path, result: &mut ScannedModuleInfo) { let contents = match std::fs::read_to_string(path) { Ok(c) => c, Err(e) => { - result.errors.push(format!("Failed to read {:?}: {}", path, e)); + result + .errors + .push(format!("Failed to read {:?}: {}", path, e)); return; } }; @@ -78,9 +84,10 @@ fn scan_rust_file(path: &Path, result: &mut ScannedModuleInfo) { for item in parsed.items { match item { Item::Struct(item_struct) => { - if let Ok(Some(info)) = try_extract_from_struct(&item_struct) { + if let Ok(Some((info, struct_name))) = try_extract_from_struct(&item_struct) { result.pda_specs.extend(info.pda_fields); result.token_specs.extend(info.token_fields); + result.rentfree_struct_names.insert(struct_name); } } Item::Mod(inner_mod) if inner_mod.content.is_some() => { @@ -102,9 +109,10 @@ fn scan_inline_module(module: &ItemMod, result: &mut ScannedModuleInfo) { for item in content { match item { Item::Struct(item_struct) => { - if let Ok(Some(info)) = try_extract_from_struct(item_struct) { + if let Ok(Some((info, struct_name))) = try_extract_from_struct(item_struct) { result.pda_specs.extend(info.pda_fields); result.token_specs.extend(info.token_fields); + result.rentfree_struct_names.insert(struct_name); } } Item::Mod(inner_mod) if inner_mod.content.is_some() => { @@ -115,8 +123,11 @@ fn scan_inline_module(module: &ItemMod, result: &mut ScannedModuleInfo) { } } -/// Try to extract rentfree info from a struct -fn try_extract_from_struct(item_struct: &ItemStruct) -> syn::Result> { +/// Try to extract rentfree info from a struct. +/// Returns (ExtractedAccountsInfo, struct_name) if the struct has rentfree fields. +fn try_extract_from_struct( + item_struct: &ItemStruct, +) -> syn::Result> { // Check if it has #[derive(Accounts)] let has_accounts_derive = item_struct.attrs.iter().any(|attr| { if attr.path().is_ident("derive") { @@ -133,7 +144,14 @@ fn try_extract_from_struct(item_struct: &ItemStruct) -> syn::Result { + let struct_name = extracted.struct_name.to_string(); + Ok(Some((extracted, struct_name))) + } + None => Ok(None), + } } /// Resolve the base path for the crate source diff --git a/sdk-libs/macros/src/compressible/instructions.rs b/sdk-libs/macros/src/compressible/instructions.rs index 9e90ec7387..47c91da186 100644 --- a/sdk-libs/macros/src/compressible/instructions.rs +++ b/sdk-libs/macros/src/compressible/instructions.rs @@ -1353,14 +1353,15 @@ fn generate_from_extracted_seeds( // COMPRESSIBLE_PROGRAM: Auto-discovers seeds from external module files // ============================================================================= -/// Main entry point for #[compressible_program] macro. +/// Main entry point for #[rentfree_program] macro. /// /// This macro reads external module files to extract seed information from -/// Accounts structs with #[compressible] fields. No explicit type list needed! +/// Accounts structs with #[rentfree] fields. It also automatically wraps +/// instruction handlers that use these Accounts structs with pre_init/finalize logic. /// /// Usage: /// ```ignore -/// #[compressible_program] +/// #[rentfree_program] /// #[program] /// pub mod my_program { /// pub mod instruction_accounts; // Macro reads this file! @@ -1369,12 +1370,97 @@ fn generate_from_extracted_seeds( /// use instruction_accounts::*; /// use state::*; /// -/// #[light_instruction] +/// // No #[light_instruction] needed - auto-wrapped! /// pub fn create_user(ctx: Context, params: Params) -> Result<()> { -/// // ... +/// // Your business logic /// } /// } /// ``` +/// Extract the Context type name from a function's parameters. +/// Returns (struct_name, params_ident) if found. +fn extract_context_and_params(fn_item: &syn::ItemFn) -> Option<(String, Ident)> { + let mut context_type = None; + let mut params_ident = None; + + for input in &fn_item.sig.inputs { + if let syn::FnArg::Typed(pat_type) = input { + if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { + // Check if this is a Context parameter + if let syn::Type::Path(type_path) = &*pat_type.ty { + if let Some(segment) = type_path.path.segments.last() { + if segment.ident == "Context" { + // Extract T from Context<'_, '_, '_, 'info, T<'info>> or Context + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + // Find the last type argument (T or T<'info>) + for arg in args.args.iter().rev() { + if let syn::GenericArgument::Type(syn::Type::Path(inner_path)) = arg { + if let Some(inner_seg) = inner_path.path.segments.last() { + context_type = Some(inner_seg.ident.to_string()); + break; + } + } + } + } + } + } + } + + // Track potential params argument (not ctx, not signer-like names) + let name = pat_ident.ident.to_string(); + if name != "ctx" && !name.contains("signer") && !name.contains("bump") { + // Prefer "params" but accept others + if name == "params" || params_ident.is_none() { + params_ident = Some(pat_ident.ident.clone()); + } + } + } + } + } + + match (context_type, params_ident) { + (Some(ctx), Some(params)) => Some((ctx, params)), + _ => None, + } +} + +/// Wrap a function with pre_init/finalize logic. +fn wrap_function_with_rentfree(fn_item: &syn::ItemFn, params_ident: &Ident) -> syn::ItemFn { + let fn_vis = &fn_item.vis; + let fn_sig = &fn_item.sig; + let fn_block = &fn_item.block; + let fn_attrs = &fn_item.attrs; + + let wrapped: syn::ItemFn = syn::parse_quote! { + #(#fn_attrs)* + #fn_vis #fn_sig { + // Phase 1: Pre-init (creates mints via CPI context write, registers compressed addresses) + use light_sdk::compressible::{LightPreInit, LightFinalize}; + let __has_pre_init = ctx.accounts.light_pre_init(ctx.remaining_accounts, &#params_ident) + .map_err(|e| { + let pe: solana_program_error::ProgramError = e.into(); + pe + })?; + + // Execute the original handler body in a closure + let __light_handler_result = (|| #fn_block)(); + + // Phase 2: On success, finalize compression + if __light_handler_result.is_ok() { + ctx.accounts.light_finalize(ctx.remaining_accounts, &#params_ident, __has_pre_init) + .map_err(|e| { + let pe: solana_program_error::ProgramError = e.into(); + pe + })?; + } + + __light_handler_result + } + }; + + wrapped +} + + #[inline(never)] pub fn compressible_program_impl( _args: TokenStream, @@ -1412,6 +1498,21 @@ pub fn compressible_program_impl( )); } + // Auto-wrap instruction handlers that use rentfree Accounts structs + if let Some((_, ref mut items)) = module.content { + for item in items.iter_mut() { + if let Item::Fn(fn_item) = item { + // Check if this function uses a rentfree Accounts struct + if let Some((context_type, params_ident)) = extract_context_and_params(fn_item) { + if scanned.rentfree_struct_names.contains(&context_type) { + // Wrap the function with pre_init/finalize logic + *fn_item = wrap_function_with_rentfree(fn_item, ¶ms_ident); + } + } + } + } + } + // Convert extracted specs to the format expected by generate_from_extracted_seeds let mut found_pda_seeds: Vec = Vec::new(); let mut found_data_fields: Vec = Vec::new(); diff --git a/sdk-libs/macros/src/finalize/instruction.rs b/sdk-libs/macros/src/finalize/instruction.rs deleted file mode 100644 index 357f9aa481..0000000000 --- a/sdk-libs/macros/src/finalize/instruction.rs +++ /dev/null @@ -1,115 +0,0 @@ -//! The #[light_instruction] attribute macro. -//! -//! Wraps instruction handlers to automatically call: -//! - `light_pre_init()` at the START (creates mints via CPI context write) -//! - `light_finalize()` at the END (compresses PDAs and executes with proof) -//! -//! This two-phase design allows mints to be used during the instruction body. - -use proc_macro2::TokenStream; -use quote::quote; -use syn::{ - parse::{Parse, ParseStream}, - spanned::Spanned, - Ident, ItemFn, -}; - -/// Arguments for #[light_instruction] or #[light_instruction(params_name)] -/// -/// If no params_name is provided, defaults to `params`. -pub struct LightInstructionArgs { - pub params_ident: Ident, -} - -impl Parse for LightInstructionArgs { - fn parse(input: ParseStream) -> syn::Result { - // If empty, default to "params" - if input.is_empty() { - return Ok(Self { - params_ident: Ident::new("params", proc_macro2::Span::call_site()), - }); - } - // Otherwise parse the identifier: #[light_instruction(my_params)] - let params_ident: Ident = input.parse()?; - Ok(Self { params_ident }) - } -} - -/// Generate the wrapped instruction function -pub fn light_instruction_impl( - args: LightInstructionArgs, - item: ItemFn, -) -> Result { - let params_ident = &args.params_ident; - let fn_vis = &item.vis; - let fn_sig = &item.sig; - let fn_block = &item.block; - let fn_attrs = &item.attrs; - - // Validate that the function has a Context parameter named `ctx` - // and a parameter matching params_ident - let mut has_ctx = false; - let mut has_params = false; - - for input in &fn_sig.inputs { - if let syn::FnArg::Typed(pat_type) = input { - if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { - if pat_ident.ident == "ctx" { - has_ctx = true; - } - if &pat_ident.ident == params_ident { - has_params = true; - } - } - } - } - - if !has_ctx { - return Err(syn::Error::new( - fn_sig.span(), - "light_instruction requires a parameter named `ctx` (the Anchor Context)", - )); - } - - if !has_params { - return Err(syn::Error::new( - params_ident.span(), - format!( - "parameter `{}` not found in function signature", - params_ident - ), - )); - } - - // Generate the wrapped function with two-phase compression: - // 1. light_pre_init() at START - creates mints via CPI context write - // 2. light_finalize() at END - compresses PDAs and executes with proof - Ok(quote! { - #(#fn_attrs)* - #fn_vis #fn_sig { - // Phase 1: Pre-init mints (writes to CPI context, does NOT execute yet) - // This allows mint accounts to be used during the instruction body - use light_sdk::compressible::{LightPreInit, LightFinalize}; - let __has_pre_init = ctx.accounts.light_pre_init(ctx.remaining_accounts, &#params_ident) - .map_err(|e| { - let pe: solana_program_error::ProgramError = e.into(); - pe - })?; - - // Execute the original handler body in a closure - let __light_handler_result = (|| #fn_block)(); - - // Phase 2: On success, finalize compression (compresses PDAs + executes proof) - // This runs BEFORE Anchor's exit() hook which serializes account data - if __light_handler_result.is_ok() { - ctx.accounts.light_finalize(ctx.remaining_accounts, &#params_ident, __has_pre_init) - .map_err(|e| { - let pe: solana_program_error::ProgramError = e.into(); - pe - })?; - } - - __light_handler_result - } - }) -} diff --git a/sdk-libs/macros/src/finalize/mod.rs b/sdk-libs/macros/src/finalize/mod.rs index b1daf31d8e..2f6b3991fc 100644 --- a/sdk-libs/macros/src/finalize/mod.rs +++ b/sdk-libs/macros/src/finalize/mod.rs @@ -1,12 +1,12 @@ -//! RentFree derive macro and light_instruction attribute macro. +//! RentFree derive macro for Accounts structs. //! //! This module provides: //! - `#[derive(RentFree)]` - Generates the LightFinalize trait impl for accounts structs //! with fields marked `#[rentfree(...)]` -//! - `#[light_instruction(params)]` - Attribute macro that auto-calls light_finalize at end of handler +//! +//! Note: Instruction handlers are auto-wrapped by `#[rentfree_program]`. mod codegen; -pub mod instruction; mod parse; use proc_macro2::TokenStream; diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 8fc8f6fdd3..64755504ef 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -2,7 +2,7 @@ extern crate proc_macro; use discriminator::discriminator; use hasher::{derive_light_hasher, derive_light_hasher_sha}; use proc_macro::TokenStream; -use syn::{parse_macro_input, DeriveInput, ItemFn, ItemStruct}; +use syn::{parse_macro_input, DeriveInput, ItemStruct}; use utils::into_token_stream; mod account; @@ -173,6 +173,9 @@ pub fn compress_as_derive(input: TokenStream) -> TokenStream { /// This macro automatically discovers #[rentfree] fields in Accounts structs /// by reading external module files. No explicit type list needed! /// +/// It also **automatically wraps** instruction handlers that use rentfree Accounts +/// structs with `light_pre_init`/`light_finalize` logic - no separate attribute needed! +/// /// Usage: /// ```ignore /// #[rentfree_program] @@ -184,9 +187,8 @@ pub fn compress_as_derive(input: TokenStream) -> TokenStream { /// use instruction_accounts::*; /// use state::*; /// -/// #[light_instruction] /// pub fn create_user(ctx: Context, params: Params) -> Result<()> { -/// // ... +/// // Your business logic /// } /// } /// ``` @@ -194,7 +196,8 @@ pub fn compress_as_derive(input: TokenStream) -> TokenStream { /// The macro: /// 1. Scans the crate's `src/` directory for `#[derive(Accounts)]` structs /// 2. Extracts seeds from `#[account(seeds = [...])]` on `#[rentfree]` fields -/// 3. Generates all necessary types, enums, and instruction handlers +/// 3. Auto-wraps instruction handlers that use those Accounts structs +/// 4. Generates all necessary types, enums, and instruction handlers /// /// Seeds are declared ONCE in Anchor attributes - no duplication! #[proc_macro_attribute] @@ -285,7 +288,7 @@ pub fn compressible_pack(input: TokenStream) -> TokenStream { into_token_stream(compressible::pack_unpack::derive_compressible_pack(input)) } -/// Consolidates all required traits for rent-free accounts into a single derive. +/// Consolidates all required traits for rent-free state accounts into a single derive. /// /// This macro is equivalent to deriving: /// - `LightHasherSha` (SHA256/ShaFlat hashing - type 3) @@ -296,11 +299,11 @@ pub fn compressible_pack(input: TokenStream) -> TokenStream { /// ## Example /// /// ```ignore -/// use light_sdk_macros::RentFree; +/// use light_sdk_macros::RentFreeAccount; /// use light_sdk::compressible::CompressionInfo; /// use solana_pubkey::Pubkey; /// -/// #[derive(Default, Debug, InitSpace, RentFree)] +/// #[derive(Default, Debug, InitSpace, RentFreeAccount)] /// #[account] /// pub struct UserRecord { /// pub owner: Pubkey, @@ -327,8 +330,8 @@ pub fn compressible_pack(input: TokenStream) -> TokenStream { /// - The `compression_info` field is auto-detected and handled (no `#[skip]` needed) /// - SHA256 (ShaFlat) hashes the entire serialized struct (no `#[hash]` needed) /// - The struct must have a `compression_info: Option` field -#[proc_macro_derive(RentFree, attributes(compress_as))] -pub fn rent_free(input: TokenStream) -> TokenStream { +#[proc_macro_derive(RentFreeAccount, attributes(compress_as))] +pub fn rent_free_account(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); into_token_stream(compressible::light_compressible::derive_light_compressible( input, @@ -455,48 +458,3 @@ pub fn rent_free_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); into_token_stream(finalize::derive_light_finalize(input)) } - -/// Attribute macro that auto-calls `light_finalize()` at end of instruction handler. -/// -/// This macro wraps your instruction handler to automatically call the -/// `LightFinalize::light_finalize()` method before returning, which executes -/// the compression CPIs. This runs BEFORE Anchor's `exit()` hook. -/// -/// ## Usage -/// -/// ```ignore -/// use anchor_lang::prelude::*; -/// use light_sdk_macros::light_instruction; -/// -/// // The argument is the name of the parameter containing compression data -/// #[light_instruction(params)] -/// pub fn create_compressible(ctx: Context, params: CompressionParams) -> Result<()> { -/// // Your business logic -/// ctx.accounts.my_account.value = params.value; -/// -/// // light_finalize() is auto-called here before returning -/// Ok(()) -/// } -/// ``` -/// -/// ## How It Works -/// -/// The macro transforms your function to: -/// 1. Execute your original function body -/// 2. On success, call `ctx.accounts.light_finalize(ctx.remaining_accounts, ¶ms)` -/// 3. Return the result -/// -/// This ensures compression CPIs run after your logic but before Anchor serializes accounts. -/// -/// ## Important Notes -/// -/// - The `params` argument must match a parameter name in your function signature -/// - Your accounts struct must derive `LightFinalize` -/// - Use `?` operator for error handling (not explicit `return Err(...)`) -/// - Errors will skip `light_finalize` and propagate normally -#[proc_macro_attribute] -pub fn light_instruction(args: TokenStream, input: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as finalize::instruction::LightInstructionArgs); - let item = parse_macro_input!(input as ItemFn); - into_token_stream(finalize::instruction::light_instruction_impl(args, item)) -} 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 445ebf724c..9cbd0f3849 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use light_sdk::{derive_light_cpi_signer, derive_light_rent_sponsor_pda}; -use light_sdk_macros::{light_instruction, rentfree_program}; +use light_sdk_macros::rentfree_program; use light_sdk_types::CpiSigner; pub mod errors; @@ -48,7 +48,6 @@ pub mod csdk_anchor_full_derived_test { FullAutoWithMintParams, LIGHT_CPI_SIGNER, }; - #[light_instruction] pub fn create_pdas_and_mint_auto<'info>( ctx: Context<'_, '_, '_, 'info, CreatePdasAndMintAuto<'info>>, params: FullAutoWithMintParams, diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state.rs index 34f696f1b1..975d41282c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state.rs @@ -1,8 +1,8 @@ use anchor_lang::prelude::*; use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::RentFree; +use light_sdk_macros::RentFreeAccount; -#[derive(Default, Debug, InitSpace, RentFree)] +#[derive(Default, Debug, InitSpace, RentFreeAccount)] #[account] pub struct UserRecord { pub compression_info: Option, @@ -13,7 +13,7 @@ pub struct UserRecord { pub category_id: u64, } -#[derive(Default, Debug, InitSpace, RentFree)] +#[derive(Default, Debug, InitSpace, RentFreeAccount)] #[compress_as(start_time = 0, end_time = None, score = 0)] #[account] pub struct GameSession { @@ -27,7 +27,7 @@ pub struct GameSession { pub score: u64, } -#[derive(Default, Debug, InitSpace, RentFree)] +#[derive(Default, Debug, InitSpace, RentFreeAccount)] #[account] pub struct PlaceholderRecord { pub compression_info: Option, From 9e16efefe9e79b23779d82322cb42546201395d0 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 16 Jan 2026 19:15:44 +0000 Subject: [PATCH 6/9] fix --- .../compressible-client/src/decompress_mint.rs | 17 +---------------- sdk-libs/compressible-client/src/lib.rs | 6 +++--- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/sdk-libs/compressible-client/src/decompress_mint.rs b/sdk-libs/compressible-client/src/decompress_mint.rs index 87dbc5fb64..12be840662 100644 --- a/sdk-libs/compressible-client/src/decompress_mint.rs +++ b/sdk-libs/compressible-client/src/decompress_mint.rs @@ -287,22 +287,7 @@ impl DecompressMintRequest { } } -/// Decompress a compressed mint with default parameters. -/// Returns empty vec if already decompressed (idempotent). -pub async fn decompress_mint( - mint_seed_pubkey: Pubkey, - fee_payer: Pubkey, - indexer: &I, -) -> Result, DecompressMintError> { - decompress_mint_idempotent( - DecompressMintRequest::new(mint_seed_pubkey), - fee_payer, - indexer, - ) - .await -} - -/// Decompresses a compressed CMint to an on-chain CMint Solana account. +/// Decompresses a compressed Mint to an on-chain Mint Solana account. /// /// This is permissionless - any fee_payer can decompress any compressed mint. /// Returns empty vec if already decompressed (idempotent). diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs index bc88797970..b124541272 100644 --- a/sdk-libs/compressible-client/src/lib.rs +++ b/sdk-libs/compressible-client/src/lib.rs @@ -30,9 +30,9 @@ pub use decompress_atas::{ pub use light_compressible::CreateAccountsProof; // Re-export TokenData for convenience (standard SPL-compatible type) pub use decompress_mint::{ - build_decompress_mint, create_mint_interface, decompress_mint, decompress_mint, - decompress_mint_idempotent, DecompressMintError, DecompressMintRequest, MintInterface, - MintState, DEFAULT_RENT_PAYMENT, DEFAULT_WRITE_TOP_UP, + build_decompress_mint, create_mint_interface, decompress_mint, decompress_mint_idempotent, + DecompressMintError, DecompressMintRequest, MintInterface, MintState, DEFAULT_RENT_PAYMENT, + DEFAULT_WRITE_TOP_UP, }; pub use initialize_config::InitializeRentFreeConfig; pub use light_token_sdk::compat::TokenData; From 5f62ffc69917f87f297074afe50a7e0c411ffd0e Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 16 Jan 2026 19:18:20 +0000 Subject: [PATCH 7/9] rm docstring --- sdk-libs/sdk/src/compressible/finalize.rs | 34 ----------------------- 1 file changed, 34 deletions(-) diff --git a/sdk-libs/sdk/src/compressible/finalize.rs b/sdk-libs/sdk/src/compressible/finalize.rs index 52bec99caf..5e24ba2daf 100644 --- a/sdk-libs/sdk/src/compressible/finalize.rs +++ b/sdk-libs/sdk/src/compressible/finalize.rs @@ -45,44 +45,10 @@ pub trait LightPreInit<'info, P> { /// Trait for finalizing compression operations on accounts. /// -/// This is generated by `#[derive(LightFinalize)]` from light-sdk-macros. -/// Use with `#[light_instruction]` attribute for automatic invocation. -/// /// # Type Parameters /// * `'info` - The account info lifetime /// * `P` - The instruction params type (from `#[instruction(params: P)]`) /// -/// # Example -/// -/// ```ignore -/// use anchor_lang::prelude::*; -/// use light_sdk::compressible::LightFinalize; -/// use light_sdk_macros::{LightFinalize, light_instruction}; -/// -/// #[derive(Accounts, LightFinalize)] -/// #[instruction(params: CompressionParams)] -/// pub struct CreateCompressible<'info> { -/// #[account(mut)] -/// pub fee_payer: Signer<'info>, -/// -/// #[account(mut)] -/// #[compressible( -/// address_tree_info = params.address_tree_info, -/// output_tree = 0 -/// )] -/// pub my_account: Account<'info, MyData>, -/// -/// /// CHECK: Compression config -/// pub compression_config: AccountInfo<'info>, -/// } -/// -/// // Auto-calls light_pre_init at start, light_finalize at end -/// #[light_instruction(params)] -/// pub fn create_compressible(ctx: Context, params: CompressionParams) -> Result<()> { -/// ctx.accounts.my_account.value = params.value; -/// Ok(()) -/// } -/// ``` pub trait LightFinalize<'info, P> { /// Execute compression finalization. /// From 484965ff69b2078f11df1aeed90f7a4dd775c568 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 16 Jan 2026 20:39:57 +0000 Subject: [PATCH 8/9] clean, fmt, lint --- program-libs/compressible/Cargo.toml | 2 +- program-libs/compressible/src/lib.rs | 1 - .../program/tests/compress_and_close.rs | 2 +- scripts/lint.sh | 2 - sdk-libs/compressible-client/DECOMPRESSION.md | 238 ---- .../compressible-client/decompress-atas.md | 619 ---------- .../compressible-client/decompress-mint.md | 514 --------- sdk-libs/compressible-client/decompress_ux.md | 284 ----- sdk-libs/compressible-client/helper.md | 114 -- sdk-libs/compressible-client/proof_helper.md | 345 ------ .../src/create_accounts_proof.rs | 6 +- .../src/decompress_atas.rs | 7 +- .../src/decompress_mint.rs | 7 +- .../src/initialize_config.rs | 6 +- sdk-libs/compressible-client/src/lib.rs | 19 +- sdk-libs/compressible-client/src/pack.rs | 13 +- sdk-libs/compressible-client/wrapper.md | 1013 ----------------- sdk-libs/macros/MACRO-NEW.md | 792 ------------- sdk-libs/macros/MACRO_REFACTOR.md | 518 --------- sdk-libs/macros/MACRO_REFACTOR_V2.md | 642 ----------- sdk-libs/macros/OPTION_A_PLAN.md | 406 ------- sdk-libs/macros/OPTION_A_STATE_FLOW.md | 316 ----- sdk-libs/macros/OVERVIEW.md | 194 ---- sdk-libs/macros/SPEC_OPTION_A.md | 546 --------- sdk-libs/macros/SPEC_OPTION_B.md | 796 ------------- .../macros/src/compressible/anchor_seeds.rs | 77 +- .../src/compressible/decompress_context.rs | 6 +- .../macros/src/compressible/file_scanner.rs | 1 + .../macros/src/compressible/instructions.rs | 89 +- .../src/compressible/light_compressible.rs | 28 +- .../macros/src/compressible/seed_providers.rs | 16 +- sdk-libs/macros/src/finalize/codegen.rs | 5 +- sdk-libs/macros/src/finalize/parse.rs | 14 +- .../src/program_test/light_program_test.rs | 6 +- .../src/compressible/decompress_runtime.rs | 3 +- .../src/compressible/decompress_runtime.rs | 15 +- sdk-libs/token-sdk/src/pack.rs | 18 +- sdk-libs/token-sdk/src/token/mod.rs | 6 +- sdk-libs/token-sdk/tests/pack_test.rs | 14 +- .../csdk-anchor-full-derived-test/SUMMARY.md | 208 ---- .../tests/basic_test.rs | 14 +- ...decompress_cmint.rs => decompress_mint.rs} | 0 .../tests/test_approve_revoke.rs | 36 +- .../sdk-light-token-test/tests/test_burn.rs | 12 +- .../tests/test_freeze_thaw.rs | 4 +- 45 files changed, 179 insertions(+), 7795 deletions(-) delete mode 100644 sdk-libs/compressible-client/DECOMPRESSION.md delete mode 100644 sdk-libs/compressible-client/decompress-atas.md delete mode 100644 sdk-libs/compressible-client/decompress-mint.md delete mode 100644 sdk-libs/compressible-client/decompress_ux.md delete mode 100644 sdk-libs/compressible-client/helper.md delete mode 100644 sdk-libs/compressible-client/proof_helper.md delete mode 100644 sdk-libs/compressible-client/wrapper.md delete mode 100644 sdk-libs/macros/MACRO-NEW.md delete mode 100644 sdk-libs/macros/MACRO_REFACTOR.md delete mode 100644 sdk-libs/macros/MACRO_REFACTOR_V2.md delete mode 100644 sdk-libs/macros/OPTION_A_PLAN.md delete mode 100644 sdk-libs/macros/OPTION_A_STATE_FLOW.md delete mode 100644 sdk-libs/macros/OVERVIEW.md delete mode 100644 sdk-libs/macros/SPEC_OPTION_A.md delete mode 100644 sdk-libs/macros/SPEC_OPTION_B.md delete mode 100644 sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md rename sdk-tests/sdk-light-token-test/src/{decompress_cmint.rs => decompress_mint.rs} (100%) diff --git a/program-libs/compressible/Cargo.toml b/program-libs/compressible/Cargo.toml index f090a5a838..20be694df9 100644 --- a/program-libs/compressible/Cargo.toml +++ b/program-libs/compressible/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" [features] default = ["solana"] solana = ["dep:solana-program-error", "light-compressed-account/solana", "light-account-checks/solana", "solana-sysvar", "solana-msg"] -anchor = ["anchor-lang", "light-compressed-account/anchor", "light-compressed-account/std", "light-account-checks/solana"] +anchor = ["anchor-lang", "light-compressed-account/anchor", "light-compressed-account/std", "light-account-checks/solana", "light-sdk-types/anchor"] pinocchio = ["dep:pinocchio", "light-compressed-account/pinocchio", "light-account-checks/pinocchio"] profile-program = [] profile-heap = ["dep:light-heap"] diff --git a/program-libs/compressible/src/lib.rs b/program-libs/compressible/src/lib.rs index 7800228cb0..dc16a63c3d 100644 --- a/program-libs/compressible/src/lib.rs +++ b/program-libs/compressible/src/lib.rs @@ -8,7 +8,6 @@ pub mod rent; use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; - use light_compressed_account::instruction_data::compressed_proof::ValidityProof; use light_sdk_types::instruction::PackedAddressTreeInfo; diff --git a/programs/compressed-token/program/tests/compress_and_close.rs b/programs/compressed-token/program/tests/compress_and_close.rs index 8b0e6e4477..ae3fa7ea89 100644 --- a/programs/compressed-token/program/tests/compress_and_close.rs +++ b/programs/compressed-token/program/tests/compress_and_close.rs @@ -5,7 +5,7 @@ use light_account_checks::{ packed_accounts::ProgramPackedAccounts, }; use light_compressed_token::compressed_token::transfer2::{ - accounts::Transfer2Accounts, compression::token::close_for_compress_and_close, + accounts::Transfer2Accounts, compression::close_for_compress_and_close, }; use light_token_interface::{ instructions::transfer2::{Compression, CompressionMode}, diff --git a/scripts/lint.sh b/scripts/lint.sh index 3358832702..d31acd7d4a 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -36,8 +36,6 @@ NO_DEFAULT_CRATES=( "light-token-sdk" "light-token-types" "light-sdk" - "sdk-compressible-test" - "csdk-anchor-derived-test" "csdk-anchor-full-derived-test" ) diff --git a/sdk-libs/compressible-client/DECOMPRESSION.md b/sdk-libs/compressible-client/DECOMPRESSION.md deleted file mode 100644 index fbc65d320a..0000000000 --- a/sdk-libs/compressible-client/DECOMPRESSION.md +++ /dev/null @@ -1,238 +0,0 @@ -# Decompression Client API - -This document describes how to decompress compressed CToken ATAs and CMints. - -## Quick Start - -```rust -use light_compressible_client::{decompress_atas, decompress_mint}; - -// Decompress ATAs -let atas = vec![ - rpc.get_ata_interface(&mint, &owner).await?, -]; -let instructions = decompress_atas(&atas, fee_payer, &rpc).await?; - -// Decompress CMint -let mint = rpc.get_mint_interface(&signer).await?; -let instructions = decompress_mint(&mint, fee_payer, &rpc).await?; -``` - -## Unified Token Data - -`AtaInterface` always provides `token_data` regardless of hot/cold state. -Uses the standard `TokenData` type from `light_token_sdk::compat`: - -```rust -let ata = rpc.get_ata_interface(&mint, &owner).await?; - -// Always works - token_data is populated from on-chain or compressed source -println!("Amount: {}", ata.token_data.amount); // Direct field access -println!("Amount: {}", ata.amount()); // Convenience method -println!("Delegate: {:?}", ata.delegate()); - -// Check state -if ata.is_cold() { - // Needs decompression -} else if ata.is_hot() { - // Already on-chain -} else { - // Doesn't exist -} -``` - -## API Overview - -### ATAs - -| Function | Description | -| ------------------------------------------------------------------- | -------------------------------------------- | -| `decompress_atas(&[AtaInterface], fee_payer, &indexer)` | High-perf wrapper: pre-fetch ATAs, call this | -| `build_decompress_atas(&[AtaInterface], fee_payer, proof)` | Sync: caller provides proof | -| `decompress_atas_idempotent(&[(mint, owner)], fee_payer, &indexer)` | Simple: fetches everything | -| `rpc.get_ata_interface(&mint, &owner)` | Fetch ATA state with unified data | - -### CMints - -| Function | Description | -| -------------------------------------------------------------- | -------------------------------------------- | -| `decompress_mint(&MintInterface, fee_payer, &indexer)` | High-perf wrapper: pre-fetch mint, call this | -| `build_decompress_mint(&MintInterface, fee_payer, proof, ...)` | Sync: caller provides proof | -| `decompress_mint(signer, fee_payer, &indexer)` | Simple: fetches everything | -| `rpc.get_mint_interface(&signer)` | Fetch CMint state | - -## Usage Patterns - -### Pattern 1: Simple (Recommended for most apps) - -Fetches state and proof internally. Easy to use. - -```rust -use light_compressible_client::{decompress_atas_idempotent, decompress_mint}; - -// Decompress ATAs by (mint, owner) pairs -let instructions = decompress_atas_idempotent( - &[(mint1, owner1), (mint2, owner2)], - fee_payer, - &rpc -).await?; - -// Decompress CMint by signer -let instructions = decompress_mint(signer, fee_payer, &rpc).await?; -``` - -### Pattern 2: High-Performance (Recommended for latency-sensitive apps) - -Pre-fetch state, then call lean wrapper. Allows batching state fetches. - -```rust -use light_compressible_client::{decompress_atas, decompress_mint}; - -// Pre-fetch ATAs (can batch with futures::join_all) -let atas = vec![ - rpc.get_ata_interface(&mint1, &owner1).await?, - rpc.get_ata_interface(&mint2, &owner2).await?, -]; - -// Access data immediately (works for both hot and cold) -for ata in &atas { - println!("ATA {} has {} tokens", ata.ata, ata.amount()); -} - -// Decompress cold ATAs (fetches proof internally, fast-exits if all hot) -let instructions = decompress_atas(&atas, fee_payer, &rpc).await?; - -// Same for mints -let mint = rpc.get_mint_interface(&signer).await?; -let instructions = decompress_mint(&mint, fee_payer, &rpc).await?; -``` - -### Pattern 3: Maximum Control (For advanced use cases) - -Pre-fetch state AND proof. Fully synchronous instruction building. - -```rust -use light_compressible_client::{build_decompress_atas, build_decompress_mint}; -use light_program_test::Indexer; - -// Pre-fetch ATAs -let atas = vec![ - rpc.get_ata_interface(&mint1, &owner1).await?, - rpc.get_ata_interface(&mint2, &owner2).await?, -]; - -// Check if any cold (sync, instant) -let cold_hashes: Vec<_> = atas.iter().filter_map(|a| a.hash()).collect(); -if cold_hashes.is_empty() { - return Ok(vec![]); // All hot - fast exit -} - -// Get proof (async) -let proof = rpc.get_validity_proof(cold_hashes, vec![], None).await?.value; - -// Build instructions (sync - no RPC) -let instructions = build_decompress_atas(&atas, fee_payer, Some(proof))?; -``` - -## Interface Types - -### AtaInterface - -```rust -pub struct AtaInterface { - pub ata: Pubkey, // ATA pubkey (derived) - pub owner: Pubkey, // Wallet owner (signer) - pub mint: Pubkey, // Token mint - pub bump: u8, // ATA bump - pub is_cold: bool, // Needs decompression? - pub token_data: TokenData, // Always present (standard SPL-compatible type) - pub raw_account: Option, // If hot - pub decompression: Option, // If cold -} - -// Standard TokenData from light_token_sdk::compat (re-exported) -pub struct TokenData { - pub mint: Pubkey, - pub owner: Pubkey, // Note: for ATAs, this is the ATA pubkey - pub amount: u64, - pub delegate: Option, - pub state: AccountState, - pub tlv: Option>, -} - -impl AtaInterface { - fn is_cold(&self) -> bool; // Needs decompression? - fn is_hot(&self) -> bool; // Already on-chain? - fn is_none(&self) -> bool; // Doesn't exist? - fn amount(&self) -> u64; // Convenience accessor - fn delegate(&self) -> Option; // Convenience accessor - fn hash(&self) -> Option<[u8; 32]>; // For proof (if cold) -} -``` - -### MintInterface - -```rust -pub struct MintInterface { - pub cmint: Pubkey, // CMint PDA - pub signer: Pubkey, // Mint signer (seed) - pub address_tree: Pubkey, // Address tree - pub compressed_address: [u8; 32], // Compressed address - pub state: MintState, // Hot/Cold/None -} - -impl MintInterface { - fn is_cold(&self) -> bool; - fn is_hot(&self) -> bool; - fn hash(&self) -> Option<[u8; 32]>; -} -``` - -## Idempotency - -All functions are idempotent: - -- Returns empty `Vec` if account is already on-chain (hot) -- Safe to call multiple times -- No errors for already-decompressed accounts - -## Example: Full Decompression Flow - -```rust -use light_compressible_client::{decompress_atas, decompress_mint}; - -async fn decompress_all( - rpc: &mut LightProgramTest, - signer: Pubkey, - mint: Pubkey, - owners: &[Pubkey], - fee_payer: Pubkey, - payer: &Keypair, -) -> Result<(), Box> { - // 1. Decompress CMint first (required for ATA decompression) - let mint_interface = rpc.get_mint_interface(&signer).await?; - if mint_interface.is_cold() { - let ix = decompress_mint(&mint_interface, fee_payer, rpc).await?; - if !ix.is_empty() { - rpc.create_and_send_transaction(&ix, &fee_payer, &[payer]).await?; - } - } - - // 2. Fetch all ATAs (can batch) - let mut atas = Vec::new(); - for owner in owners { - let ata = rpc.get_ata_interface(&mint, owner).await?; - // Data is always available - println!("Owner {} has {} tokens (cold={})", owner, ata.amount(), ata.is_cold()); - atas.push(ata); - } - - // 3. Decompress cold ATAs - let ix = decompress_atas(&atas, fee_payer, rpc).await?; - if !ix.is_empty() { - rpc.create_and_send_transaction(&ix, &fee_payer, &[payer]).await?; - } - - Ok(()) -} -``` diff --git a/sdk-libs/compressible-client/decompress-atas.md b/sdk-libs/compressible-client/decompress-atas.md deleted file mode 100644 index f713b8f3f9..0000000000 --- a/sdk-libs/compressible-client/decompress-atas.md +++ /dev/null @@ -1,619 +0,0 @@ -# Decompress ATAs Idempotent Design - -## Overview - -This document describes the SDK-only functionality to decompress multiple ATA-owned compressed token accounts in a single instruction with one proof. - -## Key Facts - -### Can we decompress multiple ATAs in one instruction with one proof? - -**YES**. This is fully supported by the existing `transfer2` instruction. - -**Why:** - -1. `get_validity_proof(hashes: Vec, ...)` accepts multiple account hashes and returns a single ZK proof covering all -2. `decompress_full_ctoken_accounts_with_indices` in `ctoken-sdk` already accepts `&[DecompressFullIndices]` for batched decompress -3. The `is_ata: bool` flag in `DecompressFullIndices` handles the ATA case correctly (owner is not marked as signer) - -### How ATA-owned compressed tokens work - -When a CToken ATA is auto-compressed: - -- The compressed token's `owner` = ATA pubkey (not wallet owner) -- `CompressedOnlyExtension.is_ata = 1` marks it as ATA-owned -- Stored in TLV: `ExtensionStruct::CompressedOnly(CompressedOnlyExtension { is_ata: 1, ... })` - -When querying the indexer: - -- Query by `owner = ATA_pubkey` (not wallet owner) -- ATA pubkey = `derive_ctoken_ata(wallet_owner, mint)` = PDA of `[wallet_owner, LIGHT_TOKEN_PROGRAM_ID, mint]` - -When decompressing: - -- Wallet owner signs the transaction (not the ATA, which is a PDA) -- `is_ata: true` in `DecompressFullIndices` ensures owner index is NOT marked as signer -- Program verifies ATA derivation: `derive_ctoken_ata(signer, mint) == compressed_owner` - -## Architecture - -### No Macro Support Required - -This is purely SDK/client-side functionality because: - -1. Direct invoke to ctoken program (no CPI from custom program) -2. Wallet owner signs (no program signing/seeds needed) -3. Standard ATA derivation (no custom seeds) -4. Existing `transfer2` instruction handles everything - -### Existing Code Reuse - -| Component | Location | Reuse | -| ----------------------- | -------------------------------------------------------- | ------ | -| ATA derivation | `ctoken-sdk/src/ctoken/create_ata.rs::derive_ctoken_ata` | Direct | -| Decompress full indices | `ctoken-sdk/src/compressed_token/v2/decompress_full.rs` | Direct | -| ATA packing | `pack_for_decompress_full_with_ata` | Direct | -| Transfer2 instruction | `create_transfer2_instruction` | Direct | -| Create ATA idempotent | `CreateAssociatedCTokenAccount::idempotent()` | Direct | -| Validity proof | `light-client::Indexer::get_validity_proof` | Direct | -| Token account query | `get_compressed_token_accounts_by_owner` | Direct | - -## API Design - -### Input: `DecompressAtaRequest` - -```rust -pub struct DecompressAtaRequest { - /// Wallet owner (signer of the transaction) - pub wallet_owner: Pubkey, - /// Token mint - pub mint: Pubkey, - /// Optional: specific compressed token account hashes to decompress - /// If None, decompress all compressed tokens for this ATA - pub hashes: Option>, -} -``` - -### Function Signature - -```rust -/// Decompresses multiple ATA-owned compressed tokens in one instruction. -/// -/// For each (wallet_owner, mint) pair: -/// 1. Derives the ATA address -/// 2. Fetches compressed token accounts owned by that ATA -/// 3. Gets a single validity proof for all accounts -/// 4. Creates destination ATAs if needed (idempotent) -/// 5. Builds single decompress instruction -/// -/// # Arguments -/// * `requests` - List of (wallet_owner, mint) pairs to decompress -/// * `fee_payer` - Fee payer pubkey -/// * `indexer` - Indexer for fetching accounts and proofs -/// -/// # Returns -/// * Vec of instructions: [create_ata_idempotent..., decompress_all] -/// * Returns empty vec if no compressed tokens found -pub async fn decompress_atas_idempotent( - requests: &[DecompressAtaRequest], - fee_payer: Pubkey, - indexer: &I, -) -> Result, CompressibleClientError>; -``` - -### Batching Rules - -1. **Single wallet, multiple mints**: Each mint requires separate ATA, but can share proof -2. **Multiple wallets**: Each wallet must sign, so typically separate transactions -3. **Same ATA, multiple compressed accounts**: Batched into single instruction (common case) - -The common use case is: user has one wallet, multiple compressed token accounts under same ATA, wants to decompress all. - -## Implementation Plan - -### Step 1: Add to `light-compressible-client` - -```rust -// In sdk-libs/compressible-client/src/lib.rs - -pub mod decompress_atas; -pub use decompress_atas::*; -``` - -### Step 2: Core Implementation - -The implementation follows the same pattern as `DecompressToCtoken::instruction()` in `ctoken-sdk/src/ctoken/decompress.rs`: - -```rust -// sdk-libs/compressible-client/src/decompress_atas.rs - -use light_client::indexer::{CompressedTokenAccount, Indexer, ValidityProofWithContext}; -use light_compressed_account::compressed_account::PackedMerkleContext; -use light_token_interface::{ - instructions::{ - extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, - transfer2::MultiInputTokenDataWithContext, - }, - state::{ExtensionStruct, TokenDataVersion}, -}; -use light_token_sdk::{ - compressed_token::{ - v2::transfer2::{ - create_transfer2_instruction, Transfer2AccountsMetaConfig, Transfer2Config, - Transfer2Inputs, - }, - CTokenAccount2, - }, - compat::AccountState, - ctoken::{derive_ctoken_ata, CreateAssociatedCTokenAccount}, -}; -use light_sdk::instruction::{PackedAccounts, PackedStateTreeInfo, ValidityProof}; -use solana_instruction::{AccountMeta, Instruction}; -use solana_pubkey::Pubkey; - -#[derive(Debug, Clone)] -pub struct DecompressAtaRequest { - pub wallet_owner: Pubkey, - pub mint: Pubkey, - /// Optional: specific hashes to decompress. If None, decompress all. - pub hashes: Option>, -} - -/// Decompresses multiple ATA-owned compressed tokens in one instruction. -/// -/// Returns (create_ata_instructions, decompress_instruction). -/// The decompress instruction is None if no compressed tokens found. -pub async fn decompress_atas_idempotent( - requests: &[DecompressAtaRequest], - fee_payer: Pubkey, - indexer: &I, -) -> Result, CompressibleClientError> { - let mut create_ata_instructions = Vec::new(); - let mut all_accounts: Vec = Vec::new(); - - // Phase 1: Gather compressed token accounts and prepare ATA creation - for request in requests { - let (ata_pubkey, ata_bump) = derive_ctoken_ata(&request.wallet_owner, &request.mint); - - // Query compressed tokens owned by this ATA - let result = indexer - .get_compressed_token_accounts_by_owner(&ata_pubkey, None, None) - .await?; - - let mut accounts = result.value.items; - if accounts.is_empty() { - continue; - } - - // Filter by hashes if specified - if let Some(ref hashes) = request.hashes { - accounts.retain(|acc| hashes.contains(&acc.account.hash)); - } - - if accounts.is_empty() { - continue; - } - - // Create ATA idempotently - let create_ata = CreateAssociatedCTokenAccount::new( - fee_payer, - request.wallet_owner, - request.mint, - ).idempotent().instruction()?; - create_ata_instructions.push(create_ata); - - // Collect context for each account - for acc in accounts { - all_accounts.push(AtaDecompressContext { - token_account: acc, - ata_pubkey, - wallet_owner: request.wallet_owner, - ata_bump, - }); - } - } - - if all_accounts.is_empty() { - return Ok(create_ata_instructions); - } - - // Phase 2: Get validity proof for all accounts - let hashes: Vec<[u8; 32]> = all_accounts - .iter() - .map(|ctx| ctx.token_account.account.hash) - .collect(); - - let proof_result = indexer - .get_validity_proof(hashes, vec![], None) - .await? - .value; - - // Phase 3: Build decompress instruction - let decompress_ix = build_batch_decompress_instruction( - fee_payer, - &all_accounts, - proof_result, - )?; - - let mut instructions = create_ata_instructions; - instructions.push(decompress_ix); - Ok(instructions) -} - -struct AtaDecompressContext { - token_account: CompressedTokenAccount, - ata_pubkey: Pubkey, - wallet_owner: Pubkey, - ata_bump: u8, -} - -fn build_batch_decompress_instruction( - fee_payer: Pubkey, - accounts: &[AtaDecompressContext], - proof: ValidityProofWithContext, -) -> Result { - let mut packed_accounts = PackedAccounts::default(); - - // Pack tree infos first (inserts trees and queues) - let packed_tree_infos = proof.pack_tree_infos(&mut packed_accounts); - let tree_infos = packed_tree_infos.state_trees.as_ref() - .ok_or(CompressibleClientError::NoStateTreesInProof)?; - - let mut token_accounts_vec = Vec::with_capacity(accounts.len()); - let mut in_tlv_data: Vec> = Vec::with_capacity(accounts.len()); - let mut has_any_tlv = false; - - for (i, ctx) in accounts.iter().enumerate() { - let token = &ctx.token_account.token; - let account = &ctx.token_account.account; - let tree_info = &tree_infos.packed_tree_infos[i]; - - // Insert wallet_owner as signer (for ATA, wallet signs, not ATA pubkey) - let owner_index = packed_accounts.insert_or_get_config(ctx.wallet_owner, true, false); - - // Insert ATA pubkey (as the token owner in TokenData - not a signer!) - let ata_index = packed_accounts.insert_or_get(ctx.ata_pubkey); - - // Insert mint - let mint_index = packed_accounts.insert_or_get(token.mint); - - // Insert delegate if present - let delegate_index = token.delegate - .map(|d| packed_accounts.insert_or_get(d)) - .unwrap_or(0); - - // Insert destination ATA - let destination_index = packed_accounts.insert_or_get(ctx.ata_pubkey); - - // Build MultiInputTokenDataWithContext - let source = MultiInputTokenDataWithContext { - owner: ata_index, // Token owner is ATA pubkey (not wallet!) - amount: token.amount, - has_delegate: token.delegate.is_some(), - delegate: delegate_index, - mint: mint_index, - version: TokenDataVersion::ShaFlat as u8, - merkle_context: PackedMerkleContext { - merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, - queue_pubkey_index: tree_info.queue_pubkey_index, - prove_by_index: account.prove_by_index, - leaf_index: account.leaf_index, - }, - root_index: tree_info.root_index, - }; - - // Build CTokenAccount2 for decompress - let mut ctoken_account = CTokenAccount2::new(vec![source.clone()])?; - ctoken_account.decompress_ctoken(token.amount, destination_index)?; - token_accounts_vec.push(ctoken_account); - - // Build TLV for this input (CompressedOnly extension for ATAs) - let is_frozen = token.state == AccountState::Frozen; - let tlv_vec: Vec = token.tlv.as_ref() - .map(|exts| { - exts.iter().filter_map(|ext| match ext { - ExtensionStruct::CompressedOnly(co) => { - Some(ExtensionInstructionData::CompressedOnly( - CompressedOnlyExtensionInstructionData { - delegated_amount: co.delegated_amount, - withheld_transfer_fee: co.withheld_transfer_fee, - is_frozen, - compression_index: 0, - is_ata: true, - bump: ctx.ata_bump, - owner_index, // Wallet owner who signs - } - )) - } - _ => None, - }).collect() - }) - .unwrap_or_default(); - - if !tlv_vec.is_empty() { - has_any_tlv = true; - } - in_tlv_data.push(tlv_vec); - } - - // Convert packed_accounts to AccountMetas - let (packed_account_metas, _, _) = packed_accounts.to_account_metas(); - - // Build Transfer2 instruction - let meta_config = Transfer2AccountsMetaConfig::new(fee_payer, packed_account_metas); - let transfer_config = Transfer2Config::default().filter_zero_amount_outputs(); - - let inputs = Transfer2Inputs { - meta_config, - token_accounts: token_accounts_vec, - transfer_config, - validity_proof: proof.proof, - in_tlv: if has_any_tlv { Some(in_tlv_data) } else { None }, - ..Default::default() - }; - - create_transfer2_instruction(inputs).map_err(CompressibleClientError::from) -} - -#[derive(Debug)] -pub enum CompressibleClientError { - Indexer(light_client::indexer::IndexerError), - CTokenSdk(light_token_sdk::error::CTokenSdkError), - NoStateTreesInProof, - ProgramError(solana_program_error::ProgramError), -} - -impl From for CompressibleClientError { - fn from(e: light_client::indexer::IndexerError) -> Self { - Self::Indexer(e) - } -} - -impl From for CompressibleClientError { - fn from(e: light_token_sdk::error::CTokenSdkError) -> Self { - Self::CTokenSdk(e) - } -} - -impl From for CompressibleClientError { - fn from(e: solana_program_error::ProgramError) -> Self { - Self::ProgramError(e) - } -} - -impl std::fmt::Display for CompressibleClientError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Indexer(e) => write!(f, "Indexer error: {:?}", e), - Self::CTokenSdk(e) => write!(f, "CToken SDK error: {:?}", e), - Self::NoStateTreesInProof => write!(f, "No state trees in proof"), - Self::ProgramError(e) => write!(f, "Program error: {:?}", e), - } - } -} - -impl std::error::Error for CompressibleClientError {} -``` - -### Step 3: Simplified Client API - -For the common case (single wallet, all compressed tokens for an ATA): - -```rust -/// Decompress all compressed tokens for a wallet's ATA -pub async fn decompress_all_for_ata( - wallet_owner: Pubkey, - mint: Pubkey, - fee_payer: Pubkey, - indexer: &I, -) -> Result, CompressibleClientError> { - decompress_atas_idempotent( - &[DecompressAtaRequest { - wallet_owner, - mint, - hashes: None, - }], - fee_payer, - indexer, - ).await -} - -/// Decompress multiple ATAs for multiple mints in one transaction -pub async fn decompress_multiple_atas( - wallet_owner: Pubkey, - mints: &[Pubkey], - fee_payer: Pubkey, - indexer: &I, -) -> Result, CompressibleClientError> { - let requests: Vec<_> = mints - .iter() - .map(|mint| DecompressAtaRequest { - wallet_owner, - mint: *mint, - hashes: None, - }) - .collect(); - - decompress_atas_idempotent(&requests, fee_payer, indexer).await -} -``` - -## Flow Diagram - -``` -User calls decompress_atas_idempotent([{wallet_owner, mint}]) - | - v -derive_ctoken_ata(wallet_owner, mint) -> ata_pubkey - | - v -indexer.get_compressed_token_accounts_by_owner(ata_pubkey) - | - v -[CompressedTokenAccount { owner: ata_pubkey, is_ata: true, ... }] - | - v -indexer.get_validity_proof([hash1, hash2, ...]) -> single proof - | - v -CreateAssociatedCTokenAccount::idempotent() -> create_ata_ix - | - v -decompress_full_ctoken_accounts_with_indices(proof, indices) -> decompress_ix - | - v -Return [create_ata_ix, decompress_ix] -``` - -## Implementation Notes - -### Key Implementation Insight - -For ATA decompress, the compressed token's `owner` field contains the ATA pubkey (not the wallet owner). However: - -1. **The wallet owner signs** the transaction (ATAs are PDAs that cannot sign) -2. **The ATA pubkey goes into TokenData.owner** (for merkle proof verification) -3. **The wallet_owner goes into CompressedOnlyExtension.owner_index** (for ATA derivation verification) - -The ctoken program verifies: `derive_ctoken_ata(owner_from_owner_index, mint) == token_data.owner` - -### Client vs On-chain Distinction - -The implementation uses `create_transfer2_instruction` directly with `Transfer2Inputs` and `CTokenAccount2`, following the same pattern as `DecompressToCtoken::instruction()` in `ctoken-sdk/src/ctoken/decompress.rs`. - -Key differences from on-chain `decompress_full_ctoken_accounts_with_indices`: - -- Uses pubkeys instead of AccountInfo -- Builds AccountMetas via `PackedAccounts::to_account_metas()` -- No CPI needed (direct invoke) - -### Error Handling - -- Return empty vec if no compressed tokens found (idempotent) -- Fail if proof generation fails -- Fail if any individual decompress fails validation - -### Transaction Size Limits - -- Each compressed account adds ~100-150 bytes to instruction data -- Practical limit: ~15-20 accounts per instruction -- For more accounts: split into multiple instructions (still fewer transactions than individual decompress) - -## Testing - -### Test Cases - -1. Single ATA with single compressed token -2. Single ATA with multiple compressed tokens (merge-like) -3. Multiple ATAs for same wallet, different mints -4. ATA already exists (idempotent create) -5. No compressed tokens (returns empty) -6. Mixed: some ATAs have tokens, some don't - -### Example Test - -```rust -#[tokio::test] -async fn test_decompress_ata_idempotent() { - // Setup: Create ATA, mint tokens, warp to compress - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - let mint = /* create mint */; - - // Create ATA and mint some tokens - let (ata_pubkey, _) = derive_ctoken_ata(&payer.pubkey(), &mint); - CreateAssociatedCTokenAccount::new(payer.pubkey(), payer.pubkey(), mint) - .instruction()?.execute(&mut rpc).await?; - - // Mint tokens to ATA - CTokenMintTo { ... }.invoke()?; - - // Warp to auto-compress - rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await?; - - // Verify ATA is closed (compressed) - assert!(rpc.get_account(ata_pubkey).await?.is_none()); - - // Verify compressed token exists owned by ATA pubkey - let compressed = rpc.get_compressed_token_accounts_by_owner(&ata_pubkey, None, None).await?; - assert_eq!(compressed.value.items.len(), 1); - assert_eq!(compressed.value.items[0].token.owner, ata_pubkey); - - // DECOMPRESS using the new API - let instructions = decompress_atas_idempotent( - &[DecompressAtaRequest { - wallet_owner: payer.pubkey(), - mint, - hashes: None, - }], - payer.pubkey(), - &rpc, - ).await?; - - // Execute - rpc.create_and_send_transaction(&instructions, &payer.pubkey(), &[&payer]).await?; - - // Verify ATA is back on-chain with balance - let ata_account = rpc.get_account(ata_pubkey).await?.unwrap(); - let ctoken = CToken::deserialize(&mut &ata_account.data[..])?; - assert_eq!(ctoken.amount, expected_amount); - - // Verify no more compressed tokens - let remaining = rpc.get_compressed_token_accounts_by_owner(&ata_pubkey, None, None).await?; - assert!(remaining.value.items.is_empty()); -} -``` - -## Comparison with PDA Decompress - -| Aspect | ATA Decompress | PDA Decompress | -| -------------------- | ------------------ | ------------------------ | -| Invoke type | Direct invoke | CPI from program | -| Signing | Wallet owner signs | Program signs with seeds | -| Seed derivation | Standard ATA | Custom per-program | -| Macro support needed | No | Yes | -| Complexity | Lower | Higher | - -## Files to Create/Modify - -1. **Create**: `sdk-libs/compressible-client/src/decompress_atas.rs` -2. **Modify**: `sdk-libs/compressible-client/src/lib.rs` (add module export) -3. **Test**: Add test in `sdk-tests/` directory - -## Dependencies - -```toml -# In sdk-libs/compressible-client/Cargo.toml -[dependencies] -light-client = { path = "../client" } -light-token-sdk = { path = "../ctoken-sdk" } -light-token-interface = { path = "../../program-libs/ctoken-interface" } -light-compressed-account = { path = "../../program-libs/compressed-account" } -light-sdk = { path = "../sdk" } -solana-pubkey = "2" -solana-instruction = "2" -solana-program-error = "2" -``` - -## What Already Exists vs What to Create - -### Already Exists (Reuse) - -| Function | Location | Purpose | -| ------------------------------------------------------------------- | ------------------------------------------------------------- | ---------------------- | -| `derive_ctoken_ata` | `ctoken-sdk/src/ctoken/create_ata.rs` | Derive ATA address | -| `CreateAssociatedCTokenAccount::idempotent()` | `ctoken-sdk/src/ctoken/create_ata.rs` | Create ATA instruction | -| `create_transfer2_instruction` | `ctoken-sdk/src/compressed_token/v2/transfer2/instruction.rs` | Build transfer2 ix | -| `CTokenAccount2::decompress_ctoken` | `ctoken-sdk/src/compressed_token/v2/account2.rs` | Set decompress mode | -| `ValidityProofWithContext::pack_tree_infos` | `light-client/src/indexer/types.rs` | Pack tree info | -| `PackedAccounts` | `light-sdk/src/instruction/packed_accounts.rs` | Account packing | -| `Transfer2Inputs`, `Transfer2Config`, `Transfer2AccountsMetaConfig` | `ctoken-sdk/src/compressed_token/v2/transfer2/` | Transfer2 config | - -### To Create - -| Function | Location | Purpose | -| ---------------------------- | -------------------------------------------- | --------------- | -| `decompress_atas_idempotent` | `compressible-client/src/decompress_atas.rs` | Main API | -| `decompress_all_for_ata` | `compressible-client/src/decompress_atas.rs` | Convenience API | -| `decompress_multiple_atas` | `compressible-client/src/decompress_atas.rs` | Multi-mint API | -| `CompressibleClientError` | `compressible-client/src/decompress_atas.rs` | Error type | diff --git a/sdk-libs/compressible-client/decompress-mint.md b/sdk-libs/compressible-client/decompress-mint.md deleted file mode 100644 index 877e276dca..0000000000 --- a/sdk-libs/compressible-client/decompress-mint.md +++ /dev/null @@ -1,514 +0,0 @@ -# Decompress Mint SDK Design - -## Overview - -SDK-only functionality to decompress compressed CMint accounts (mints that were created via `#[compressible]` macro and have been auto-compressed by forester). - -## Key Facts - -### Can we build this purely in SDK without macro changes? - -**YES**. This is fully supported by the existing `DecompressMint` instruction builder in `ctoken-sdk`. - -**Why:** - -1. **DecompressMint is permissionless** - the authority signer is required by the instruction format but NOT validated against `mint_authority`. Anyone can decompress any compressed mint. - -2. **mint_seed does NOT need to sign** - uses `with_mint_signer_no_sign()` internally. The mint_seed is only used for PDA derivation. - -3. `DecompressMint` struct already exists in `ctoken-sdk/src/ctoken/decompress_mint.rs` with a complete `instruction()` method. - -4. All data needed is queryable from the indexer via the compressed mint's address. - -### Proof from on-chain code - -From `programs/compressed-token/program/src/compressed_token/mint_action/actions/decompress_mint.rs`: - -```rust -/// DecompressMint is **permissionless** - the caller pays initial rent, rent exemption is sponsored by the rent_sponsor. -/// The authority signer is still required for MintAction, but does not need to match mint_authority. -``` - -From `process_actions.rs` - DecompressMint does NOT call `check_authority()`: - -```rust -ZAction::DecompressMint(decompress_action) => { - // Note: No check_authority() call - permissionless! - process_decompress_mint_action( - decompress_action, - compressed_mint, - validated_accounts, - mint_signer, - fee_payer, - )?; -} -``` - -### How program-created compressed mints work - -When a mint is created via `#[compressible]` macro (like in `csdk-anchor-full-derived-test`): - -1. **mint_seed_pubkey** = A program PDA (e.g., `LP_MINT_SIGNER_SEED + authority`) -2. **CMint PDA** = `find_mint_address(mint_seed_pubkey)` = PDA of `[COMPRESSED_MINT_SEED, mint_seed_pubkey]` under ctoken program -3. **Compressed address** = `derive_cmint_compressed_address(mint_seed_pubkey, address_tree)` - -When querying the indexer: - -- Query by `address = compressed_address` (derived from mint_seed_pubkey + address_tree) -- OR query by `cmint` pubkey if known - -When decompressing: - -- **Any signer can call** (permissionless) -- mint_seed_pubkey passed for CMint PDA derivation (does NOT sign) -- Fee payer pays for rent and top-up - -## Architecture - -### No CPI Required - -Unlike PDAs which require program signing for decompression, CMint decompression is: - -1. **Direct invoke** to ctoken program -2. **Permissionless** - any signer works as authority -3. **No custom seeds** needed from the caller program - -### Existing Code Reuse - -| Component | Location | Reuse | -| ------------------------------------- | -------------------------------------------------------------------------- | ------ | -| `DecompressMint` struct | `ctoken-sdk/src/ctoken/decompress_mint.rs` | Direct | -| `find_mint_address` | `ctoken-sdk/src/ctoken/create_cmint.rs` | Direct | -| `derive_cmint_compressed_address` | `ctoken-sdk/src/compressed_token/v2/create_compressed_mint/instruction.rs` | Direct | -| `CompressedMintWithContext` | `ctoken-interface/src/instructions/mint_action/instruction_data.rs` | Direct | -| `get_compressed_account` | `light-client/src/indexer/indexer_trait.rs` | Direct | -| `get_validity_proof` | `light-client/src/indexer/indexer_trait.rs` | Direct | -| `config_pda()` / `rent_sponsor_pda()` | `ctoken-sdk/src/ctoken/mod.rs` | Direct | - -## API Design - -### Input: `DecompressMintRequest` - -```rust -#[derive(Debug, Clone)] -pub struct DecompressMintRequest { - /// The seed pubkey used to derive the CMint PDA. - /// This is the same value passed as `mint_signer` when the mint was created. - pub mint_seed_pubkey: Pubkey, - /// Address tree where the compressed mint was created. - /// If None, uses the default cmint address tree. - pub address_tree: Option, - /// Rent payment in epochs (must be 0 or >= 2). Default: 2 - pub rent_payment: Option, - /// Lamports for future write operations. Default: 766 - pub write_top_up: Option, -} -``` - -### Primary Function Signature - -```rust -/// Decompresses a compressed CMint to an on-chain CMint Solana account. -/// -/// This is permissionless - any fee_payer can decompress any compressed mint. -/// The mint_seed_pubkey is used to derive the CMint PDA and compressed address. -/// -/// # Arguments -/// * `request` - Decompress mint parameters -/// * `fee_payer` - Fee payer who pays rent and top-up -/// * `indexer` - Indexer for fetching compressed account and proof -/// -/// # Returns -/// * Vec with decompress instruction if mint needs decompressing -/// * Empty vec if mint is already decompressed (idempotent) -/// * Error only for actual failures (not found, indexer errors) -pub async fn decompress_mint_idempotent( - request: DecompressMintRequest, - fee_payer: Pubkey, - indexer: &I, -) -> Result, DecompressMintError>; -``` - -### Simplified API - -For common case with defaults: - -```rust -/// Decompress a compressed mint with default parameters. -/// -/// Uses default address tree, rent_payment=2, write_top_up=766. -/// Returns empty vec if already decompressed (idempotent). -pub async fn decompress_mint( - mint_seed_pubkey: Pubkey, - fee_payer: Pubkey, - indexer: &I, -) -> Result, DecompressMintError>; -``` - -### Error Type - -```rust -#[derive(Debug, Error)] -pub enum DecompressMintError { - #[error("Indexer error: {0}")] - Indexer(#[from] IndexerError), - - #[error("Compressed mint not found for seed {mint_seed:?}")] - MintNotFound { mint_seed: Pubkey }, - - #[error("Missing compressed mint data in account")] - MissingMintData, - - #[error("Program error: {0}")] - ProgramError(#[from] ProgramError), -} -``` - -Note: `AlreadyDecompressed` is NOT an error - returns empty vec instead (idempotent behavior). - -## Implementation - -### File: `sdk-libs/compressible-client/src/decompress_mint.rs` - -```rust -use light_client::indexer::{Indexer, IndexerError}; -use light_compressed_account::instruction_data::compressed_proof::ValidityProof; -use light_token_interface::{ - instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, - state::CompressedMint, - CMINT_ADDRESS_TREE, -}; -use light_token_sdk::token::{ - derive_cmint_compressed_address, DecompressMint, -}; -use solana_instruction::Instruction; -use solana_program_error::ProgramError; -use solana_pubkey::Pubkey; -use thiserror::Error; - -#[derive(Debug, Error)] -pub enum DecompressMintError { - #[error("Indexer error: {0}")] - Indexer(#[from] IndexerError), - - #[error("Compressed mint not found for seed {mint_seed:?}")] - MintNotFound { mint_seed: Pubkey }, - - #[error("Missing compressed mint data in account")] - MissingMintData, - - #[error("Program error: {0}")] - ProgramError(#[from] ProgramError), -} - -#[derive(Debug, Clone)] -pub struct DecompressMintRequest { - pub mint_seed_pubkey: Pubkey, - pub address_tree: Option, - pub rent_payment: Option, - pub write_top_up: Option, -} - -impl DecompressMintRequest { - pub fn new(mint_seed_pubkey: Pubkey) -> Self { - Self { - mint_seed_pubkey, - address_tree: None, - rent_payment: None, - write_top_up: None, - } - } - - pub fn with_address_tree(mut self, address_tree: Pubkey) -> Self { - self.address_tree = Some(address_tree); - self - } - - pub fn with_rent_payment(mut self, rent_payment: u8) -> Self { - self.rent_payment = Some(rent_payment); - self - } - - pub fn with_write_top_up(mut self, write_top_up: u32) -> Self { - self.write_top_up = Some(write_top_up); - self - } -} - -/// Default rent payment in epochs (~24 hours per epoch) -pub const DEFAULT_RENT_PAYMENT: u8 = 2; -/// Default write top-up lamports (~3 hours rent per write) -pub const DEFAULT_WRITE_TOP_UP: u32 = 766; - -/// Decompress a compressed mint with default parameters. -/// Returns empty vec if already decompressed (idempotent). -pub async fn decompress_mint( - mint_seed_pubkey: Pubkey, - fee_payer: Pubkey, - indexer: &I, -) -> Result, DecompressMintError> { - decompress_mint_idempotent(DecompressMintRequest::new(mint_seed_pubkey), fee_payer, indexer) - .await -} - -/// Decompresses a compressed CMint to an on-chain CMint Solana account. -/// -/// This is permissionless - any fee_payer can decompress any compressed mint. -/// Returns empty vec if already decompressed (idempotent). -pub async fn decompress_mint_idempotent( - request: DecompressMintRequest, - fee_payer: Pubkey, - indexer: &I, -) -> Result, DecompressMintError> { - // 1. Derive addresses - let address_tree = request - .address_tree - .unwrap_or(Pubkey::new_from_array(CMINT_ADDRESS_TREE)); - let compressed_address = - derive_cmint_compressed_address(&request.mint_seed_pubkey, &address_tree); - - // 2. Fetch compressed mint account from indexer - let compressed_account = indexer - .get_compressed_account(compressed_address, None) - .await? - .value - .ok_or(DecompressMintError::MintNotFound { - mint_seed: request.mint_seed_pubkey, - })?; - - // 3. Parse mint data from compressed account - let mint_data = parse_compressed_mint_data(&compressed_account)?; - - // 4. Check if already decompressed - return empty vec (idempotent) - if mint_data.metadata.cmint_decompressed { - return Ok(vec![]); - } - - // 5. Get validity proof - let proof_result = indexer - .get_validity_proof(vec![compressed_account.hash], vec![], None) - .await? - .value; - - // 6. Extract tree info from proof result - let account_info = &proof_result.accounts[0]; - let state_tree = account_info.tree_info.tree; - let input_queue = account_info.tree_info.queue; - let output_queue = account_info - .tree_info - .next_tree_info - .as_ref() - .map(|next| next.queue) - .unwrap_or(input_queue); - - // 7. Build CompressedMintWithContext - let mint_instruction_data = CompressedMintInstructionData::try_from(mint_data) - .map_err(|_| DecompressMintError::MissingMintData)?; - - let compressed_mint_with_context = CompressedMintWithContext { - leaf_index: compressed_account.leaf_index, - prove_by_index: compressed_account.prove_by_index, - root_index: account_info - .root_index - .root_index() - .unwrap_or_default(), - address: compressed_address, - mint: Some(mint_instruction_data), - }; - - // 8. Build DecompressMint instruction - let decompress = DecompressMint { - mint_seed_pubkey: request.mint_seed_pubkey, - payer: fee_payer, - authority: fee_payer, // Permissionless - any signer works - state_tree, - input_queue, - output_queue, - compressed_mint_with_context, - proof: ValidityProof(proof_result.proof.into()), - rent_payment: request.rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT), - write_top_up: request.write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP), - }; - - let ix = decompress.instruction().map_err(DecompressMintError::from)?; - Ok(vec![ix]) -} - -/// Parse CompressedMint from compressed account data. -fn parse_compressed_mint_data( - account: &light_client::indexer::CompressedAccount, -) -> Result { - use borsh::BorshDeserialize; - - let data = account - .data - .as_ref() - .ok_or(DecompressMintError::MissingMintData)?; - - CompressedMint::try_from_slice(&data.data) - .map_err(|_| DecompressMintError::MissingMintData) -} -``` - -### Module Export - -Update `sdk-libs/compressible-client/src/lib.rs`: - -```rust -pub mod decompress_atas; -pub mod decompress_mint; -pub mod get_compressible_account; - -pub use decompress_atas::*; -pub use decompress_mint::*; -``` - -## Flow Diagram - -``` -User calls decompress_mint(mint_seed_pubkey, fee_payer, indexer) - | - v -derive_cmint_compressed_address(mint_seed_pubkey, address_tree) -> compressed_address - | - v -indexer.get_compressed_account(compressed_address) -> CompressedAccount { data, hash, tree_info } - | - v -parse_compressed_mint_data() -> CompressedMint { metadata.cmint_decompressed? } - | - +--[if cmint_decompressed == true]--> Return empty vec (idempotent) - | - v [if cmint_decompressed == false] -indexer.get_validity_proof([hash]) -> ValidityProofWithContext - | - v -DecompressMint { - mint_seed_pubkey, - payer: fee_payer, - authority: fee_payer, // Permissionless! - ... -}.instruction() -> Instruction - | - v -Return vec![instruction] (caller signs and sends) -``` - -## Comparison with Other Decompress APIs - -| Aspect | Mint Decompress | ATA Decompress | PDA Decompress | -| --------------- | --------------------------- | ------------------ | ------------------------ | -| Invoke type | Direct invoke | Direct invoke | CPI from program | -| Signing | Permissionless (any signer) | Wallet owner signs | Program signs with seeds | -| Seed derivation | mint_seed -> CMint PDA | Standard ATA | Custom per-program | -| Macro support | No | No | Yes | -| Complexity | Low | Low | Higher | -| Authority check | None | ATA derivation | PDA derivation | - -## Implementation Notes - -### Why Permissionless? - -Decompressing a mint doesn't change ownership or authority - it just brings the data back on-chain. The same mint_authority retains control. Anyone paying rent can "warm up" a cold mint. - -### Idempotency - -The function checks `cmint_decompressed` flag: - -- If false: returns vec with decompress instruction -- If true: returns empty vec (nothing to do) - -This follows the same pattern as `decompress_atas_idempotent` - callers can safely call multiple times without error handling. - -### Transaction Execution - -The returned instructions require **only the fee_payer to sign**: - -```rust -let instructions = decompress_mint(mint_seed, fee_payer, &indexer).await?; -if !instructions.is_empty() { - rpc.create_and_send_transaction(&instructions, &fee_payer, &[&fee_payer_keypair]).await?; -} -// If empty, mint was already decompressed - nothing to do -``` - -### Error Handling - -- `MintNotFound`: Compressed mint doesn't exist (never created or wrong address_tree) -- `AlreadyDecompressed`: Mint is already on-chain (idempotent case) -- `MissingMintData`: Compressed account exists but has no mint data (shouldn't happen) - -## Dependencies - -```toml -# In sdk-libs/compressible-client/Cargo.toml -[dependencies] -light-client = { path = "../client" } -light-token-sdk = { path = "../ctoken-sdk" } -light-token-interface = { path = "../../program-libs/ctoken-interface" } -light-compressed-account = { path = "../../program-libs/compressed-account" } -solana-pubkey = "2" -solana-instruction = "2" -solana-program-error = "2" -thiserror = "1.0" -``` - -## Testing - -### Test Cases - -1. **Basic decompress**: Compressed mint -> on-chain CMint -2. **Already decompressed**: Returns `AlreadyDecompressed` error -3. **Not found**: Returns `MintNotFound` error -4. **Custom address tree**: Works with non-default address tree -5. **Custom rent/top-up**: Respects custom parameters - -### Integration Test Pattern - -```rust -#[tokio::test] -async fn test_decompress_mint() { - // Setup: Create mint via program, warp to compress - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - // Create mint via csdk-anchor-full-derived-test - let (mint_signer_pda, _) = Pubkey::find_program_address( - &[LP_MINT_SIGNER_SEED, authority.pubkey().as_ref()], - &program_id, - ); - let (cmint_pda, _) = find_mint_address(&mint_signer_pda); - - // ... execute create_pdas_and_mint_auto ... - - // Verify mint exists on-chain - assert!(rpc.get_account(cmint_pda).await?.is_some()); - - // Warp to auto-compress - rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await?; - - // Verify mint is compressed (closed on-chain) - assert!(rpc.get_account(cmint_pda).await?.is_none()); - - // DECOMPRESS using the new API - let instructions = decompress_mint(mint_signer_pda, payer.pubkey(), &rpc).await?; - assert_eq!(instructions.len(), 1); // Should have one decompress instruction - - // Execute (only fee_payer signs) - rpc.create_and_send_transaction(&instructions, &payer.pubkey(), &[&payer]).await?; - - // Verify mint is back on-chain - assert!(rpc.get_account(cmint_pda).await?.is_some()); - - // Verify calling again returns empty vec (idempotent) - let instructions = decompress_mint(mint_signer_pda, payer.pubkey(), &rpc).await?; - assert!(instructions.is_empty()); // Already decompressed, nothing to do -} -``` - -## Files to Create/Modify - -1. **Create**: `sdk-libs/compressible-client/src/decompress_mint.rs` -2. **Modify**: `sdk-libs/compressible-client/src/lib.rs` (add module export) -3. **Modify**: `sdk-libs/compressible-client/Cargo.toml` (add dependencies if missing) -4. **Test**: Add integration test to `sdk-tests/` directory diff --git a/sdk-libs/compressible-client/decompress_ux.md b/sdk-libs/compressible-client/decompress_ux.md deleted file mode 100644 index 479e63c44f..0000000000 --- a/sdk-libs/compressible-client/decompress_ux.md +++ /dev/null @@ -1,284 +0,0 @@ -# Decompress UX Improvement: `from_seeds` Pattern - -## Problem - -Current decompress flow is verbose and redundant: - -```rust -// 1. Create interface -let user_interface = AccountInterface::cold(user_record_pda, compressed_user.clone()); - -// 2. Extract data, call constructor, pass seeds -let user_variant = CompressedAccountVariant::user_record( - user_interface.compressed_data().unwrap(), // extract data - UserRecordSeeds { authority, mint_authority, owner, category_id }, -)?; - -// 3. Combine interface + variant -RentFreeDecompressAccount::new(user_interface, user_variant) -``` - -**Issues:** - -- 3 separate steps per account -- `compressed_data()` extraction is boilerplate -- Interface passed twice conceptually (once for data, once for wrapper) - -## Solution: `from_seeds` Pattern - -```rust -RentFreeDecompressAccount::from_seeds( - AccountInterface::cold(user_record_pda, compressed_user), - UserRecordSeeds { authority, mint_authority, owner, category_id }, -)? -``` - -**Single call.** The `Seeds` type (already generated by macro) tells us which variant to construct. - -## Design - -### 1. Trait Definition (SBF-compatible) - -Location: `light-sdk/src/compressible/mod.rs` - -```rust -/// Trait for seeds that can construct a compressed account variant. -/// Implemented by generated `XxxSeeds` structs. -pub trait IntoVariant { - /// Construct variant from compressed account data bytes and these seeds. - fn into_variant(self, data: &[u8]) -> Result; -} -``` - -This trait is SBF-compatible because: - -- No client-crate dependencies -- Just takes `&[u8]` and returns variant -- Lives in `light-sdk` which is already program-side - -### 2. Macro Generates Trait Impl - -Location: `sdk-libs/macros/src/compressible/instructions.rs` - -Currently generates: - -```rust -pub struct UserRecordSeeds { - pub authority: Pubkey, - pub mint_authority: Pubkey, - pub owner: Pubkey, - pub category_id: u64, -} - -impl CompressedAccountVariant { - pub fn user_record(data: &[u8], seeds: UserRecordSeeds) -> Result { - // deserialize, verify seeds, construct variant - } -} -``` - -**Add trait impl:** - -```rust -impl light_sdk::compressible::IntoVariant for UserRecordSeeds { - fn into_variant(self, data: &[u8]) -> Result { - CompressedAccountVariant::user_record(data, self) - } -} -``` - -### 3. Client Helper Method - -Location: `light-compressible-client/src/lib.rs` - -```rust -impl RentFreeDecompressAccount { - /// Create decompression request from account interface and seeds. - /// - /// The seeds type determines which variant constructor to call. - /// Data is extracted from interface, passed to `IntoVariant::into_variant()`. - pub fn from_seeds( - interface: AccountInterface, - seeds: S, - ) -> Result - where - S: light_sdk::compressible::IntoVariant, - { - let data = interface - .compressed_data() - .ok_or_else(|| anchor_lang::error::Error::from( - anchor_lang::error::ErrorCode::AccountNotInitialized - ))?; - let variant = seeds.into_variant(data)?; - Ok(Self::new(interface, variant)) - } -} -``` - -### 4. CToken Handling - -CToken accounts store `TokenData` in their compressed bytes. Parse internally - no separate `token_data` param needed. - -```rust -impl RentFreeDecompressAccount { - /// Create decompression request for CToken account. - /// Parses TokenData from interface.compressed_data() internally. - pub fn from_ctoken( - interface: AccountInterface, - ctoken_variant: T, - ) -> Result - where - T: IntoCTokenVariant, - { - let data = interface.compressed_data() - .ok_or(Error::AccountNotCompressed)?; - let token_data = TokenData::try_from_slice(data)?; - let variant = ctoken_variant.into_ctoken_variant(token_data); - Ok(Self::new(interface, variant)) - } -} -``` - -**Trait (generated by macro):** - -```rust -pub trait IntoCTokenVariant { - fn into_ctoken_variant(self, token_data: TokenData) -> V; -} - -// Generated by macro -impl IntoCTokenVariant for TokenAccountVariant { - fn into_ctoken_variant(self, token_data: TokenData) -> CompressedAccountVariant { - CompressedAccountVariant::CTokenData(CTokenData { - variant: self, - token_data, - }) - } -} -``` - -Usage: - -```rust -RentFreeDecompressAccount::from_ctoken( - AccountInterface::cold(vault_pda, compressed_vault.account), - TokenAccountVariant::Vault { cmint: cmint_pda }, -)? -``` - -## Final API - -### Before (verbose) - -```rust -let user_interface = AccountInterface::cold(user_record_pda, compressed_user.clone()); -let game_interface = AccountInterface::cold(game_session_pda, compressed_game.clone()); -let vault_interface = AccountInterface::cold(vault_pda, compressed_vault.account.clone()); - -let user_variant = CompressedAccountVariant::user_record( - user_interface.compressed_data().unwrap(), - UserRecordSeeds { authority, mint_authority, owner, category_id }, -)?; -let game_variant = CompressedAccountVariant::game_session( - game_interface.compressed_data().unwrap(), - GameSessionSeeds { user, authority, session_id }, -)?; -let vault_ctoken_data = CTokenData { - variant: TokenAccountVariant::Vault { cmint: cmint_pda }, - token_data: compressed_vault.token.clone(), -}; - -let decompress_accounts = vec![ - RentFreeDecompressAccount::new(user_interface, user_variant), - RentFreeDecompressAccount::new(game_interface, game_variant), - RentFreeDecompressAccount::new(vault_interface, CompressedAccountVariant::CTokenData(vault_ctoken_data)), -]; -``` - -### After (clean) - -```rust -let decompress_accounts = vec![ - RentFreeDecompressAccount::from_seeds( - AccountInterface::cold(user_record_pda, compressed_user), - UserRecordSeeds { authority, mint_authority, owner, category_id }, - )?, - RentFreeDecompressAccount::from_seeds( - AccountInterface::cold(game_session_pda, compressed_game), - GameSessionSeeds { user, authority, session_id }, - )?, - RentFreeDecompressAccount::from_ctoken( - AccountInterface::cold(vault_pda, compressed_vault.account), - TokenAccountVariant::Vault { cmint: cmint_pda }, - )?, -]; -``` - -**Reduction:** ~25 lines → ~12 lines (52% less) -**Cognitive load:** 3 concepts → 1 concept per account -**Redundant data passing:** Eliminated - -## Implementation Checklist - -1. **Add `IntoVariant` trait to `light-sdk`** - - File: `sdk-libs/sdk/src/compressible/mod.rs` - - SBF-compatible, no client deps - -2. **Add `IntoCTokenVariant` trait to `light-sdk`** - - Same file - - For CToken variant construction - -3. **Update macro to generate `IntoVariant` impl** - - File: `sdk-libs/macros/src/compressible/instructions.rs` - - Add impl alongside existing `UserRecordSeeds` struct - -4. **Update macro to generate `IntoCTokenVariant` impl** - - Same file - - For `TokenAccountVariant` - -5. **Add `from_seeds` method to `RentFreeDecompressAccount`** - - File: `sdk-libs/compressible-client/src/lib.rs` - - Uses trait bound `S: IntoVariant` - -6. **Add `from_ctoken` method to `RentFreeDecompressAccount`** - - Same file - - Uses trait bound `T: IntoCTokenVariant` - -7. **Update test to use new API** - - File: `sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs` - -## Compatibility - -- **SBF programs:** No changes needed. Traits in `light-sdk` are SBF-compatible. -- **Existing code:** `::new()` still works. `from_seeds` is additive. -- **Migration:** Optional. Users can adopt incrementally. - -## Error Handling - -Both methods return `Result` because: - -**`from_seeds`:** - -1. `compressed_data()` might be `None` (hot account passed to cold-only method) -2. `into_variant()` can fail (seed verification, deserialization) - -**`from_ctoken`:** - -1. `compressed_data()` might be `None` (hot account passed) -2. `TokenData::try_from_slice()` can fail (malformed data) - -## Rating: 9/10 - -### Pros - -- **Consistent**: Both use `AccountInterface` first arg -- **Minimal**: Single call per account, no intermediate vars -- **Type-safe**: Traits enforce correct mapping -- **SBF-compatible**: Traits in light-sdk, impl in macro -- **Clear intent**: `from_seeds` vs `from_ctoken` - -### Cons - -- CToken still needs `.account` extraction from `CompressedTokenAccount` -- Re-parses `TokenData` from bytes (indexer already parsed, but keeps API uniform) -- Two traits to maintain (hidden from user) diff --git a/sdk-libs/compressible-client/helper.md b/sdk-libs/compressible-client/helper.md deleted file mode 100644 index 8992852849..0000000000 --- a/sdk-libs/compressible-client/helper.md +++ /dev/null @@ -1,114 +0,0 @@ -# Compressed Account Client Helper - -## The Problem - -Building remaining accounts is verbose and error-prone: - -```rust -// Current: 10+ lines of boilerplate every time -let mut packed = PackedAccounts::default(); -let system_config = match cpi_context { - Some(ctx) => SystemAccountMetaConfig::new_with_cpi_context(program_id, ctx), - None => SystemAccountMetaConfig::new(program_id), -}; -packed.add_system_accounts_v2(system_config)?; -let output_queue = tree_info.next_tree_info.as_ref().map(|n| n.queue).unwrap_or(tree_info.queue); -let output_tree_index = packed.insert_or_get(output_queue); -let packed_trees = proof.pack_tree_infos(&mut packed); -let (remaining_accounts, system_offset, _) = packed.to_account_metas(); -``` - -## The Solution - -One function: - -```rust -pub struct PackedProofResult { - /// Remaining accounts to append to your instruction's accounts. - pub remaining_accounts: Vec, - /// Packed tree infos. Use `.address_trees` or `.state_trees` as needed. - pub packed_tree_infos: PackedTreeInfos, - /// Index of output tree in remaining accounts. - pub output_tree_index: u8, - /// Offset where system accounts start (if needed). - pub system_accounts_offset: u8, -} - -/// Packs validity proof into remaining accounts. -/// -/// # Arguments -/// - `program_id`: Your program ID -/// - `proof`: From `get_validity_proof()` -/// - `output_tree`: From `get_random_state_tree_info()` -/// - `cpi_context`: `tree_info.cpi_context` when mixing PDAs+tokens, else `None` -pub fn pack_proof( - program_id: &Pubkey, - proof: ValidityProofWithContext, - output_tree: &TreeInfo, - cpi_context: Option, -) -> Result; -``` - -## Full Flow - -```rust -// 1. Derive addresses (use existing functions) -let user_addr = derive_address(&user_pda.to_bytes(), &tree.to_bytes(), &program_id.to_bytes()); -let mint_addr = derive_cmint_compressed_address(&mint_signer, &tree); - -// 2. Get proof + output tree -let proof = rpc.get_validity_proof( - vec![], // existing hashes (empty for new accounts) - vec![ // new addresses - AddressWithTree { address: user_addr, tree }, - AddressWithTree { address: mint_addr, tree }, - ], - None, -).await?.value; -let output_tree = rpc.get_random_state_tree_info()?; - -// 3. Pack (the helper) -let packed = pack_proof( - &program_id, - proof.clone(), - &output_tree, - output_tree.cpi_context, // Some for mixed PDA+token, None for PDA-only -)?; - -// 4. Build instruction -let ix = Instruction { - program_id, - accounts: [my_accounts.to_account_metas(None), packed.remaining_accounts].concat(), - data: MyInstruction { - proof: proof.proof, - address_tree_infos: packed.packed_tree_infos.address_trees, - output_tree_index: packed.output_tree_index, - // ... - }.data(), -}; -``` - -## When to use CPI context - -``` -PDA-only tx → cpi_context: None -Token-only tx → cpi_context: None -Mixed PDA + token → cpi_context: tree_info.cpi_context (Option) -``` - -## Errors - -```rust -#[derive(Debug, Error)] -pub enum PackError { - #[error("Failed to add system accounts: {0}")] - SystemAccounts(#[from] LightSdkError), -} -``` - -## Files - -| File | Contents | -| ------------- | ------------------------------------------------ | -| `src/pack.rs` | `pack_proof()`, `PackedProofResult`, `PackError` | -| `src/lib.rs` | Re-export | diff --git a/sdk-libs/compressible-client/proof_helper.md b/sdk-libs/compressible-client/proof_helper.md deleted file mode 100644 index db0024a65f..0000000000 --- a/sdk-libs/compressible-client/proof_helper.md +++ /dev/null @@ -1,345 +0,0 @@ -# get_create_accounts_proof Helper Specification - -## Problem - -Creating compressed accounts (INIT flow) requires verbose boilerplate: - -```rust -// Current: 30+ lines every time -let address_tree_pubkey = rpc.get_address_tree_v2().tree; -let state_tree_info = rpc.get_random_state_tree_info().unwrap(); - -// Derive each address manually -let user_address = derive_address( - &user_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), -); -let game_address = derive_address( - &game_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), -); -let mint_address = derive_cmint_compressed_address(&mint_signer, &address_tree_pubkey); - -// Build AddressWithTree for each -let rpc_result = rpc.get_validity_proof( - vec![], - vec![ - AddressWithTree { address: user_address, tree: address_tree_pubkey }, - AddressWithTree { address: game_address, tree: address_tree_pubkey }, - AddressWithTree { address: mint_address, tree: address_tree_pubkey }, - ], - None, -).await?.value; - -// Pack proof -let packed = pack_proof(&program_id, rpc_result.clone(), &state_tree_info, state_tree_info.cpi_context)?; -let user_tree_info = packed.packed_tree_infos.address_trees[0]; -let game_tree_info = packed.packed_tree_infos.address_trees[1]; -let mint_tree_info = packed.packed_tree_infos.address_trees[2]; -``` - -## Solution - -One opinionated helper for the INIT flow: - -```rust -/// Input for creating new compressed accounts. -/// program_id from main function is used as default owner for Pda variant. -pub enum CreateAccountsProofInput { - /// PDA owned by the calling program (uses program_id from main fn) - Pda(Pubkey), - /// PDA with explicit owner (for cross-program accounts) - PdaWithOwner { pda: Pubkey, owner: Pubkey }, - /// CMint (always uses LIGHT_TOKEN_PROGRAM_ID internally) - Mint(Pubkey), -} - -impl CreateAccountsProofInput { - /// Standard PDA owned by calling program. - /// Address derived: derive_address(&pda, &tree, &program_id) - pub fn pda(pda: Pubkey) -> Self { - Self::Pda(pda) - } - - /// PDA with explicit owner (rare: cross-program accounts). - /// Address derived: derive_address(&pda, &tree, &owner) - pub fn pda_with_owner(pda: Pubkey, owner: Pubkey) -> Self { - Self::PdaWithOwner { pda, owner } - } - - /// Compressed mint (CMint). - /// Address derived: derive_cmint_compressed_address(&mint_signer, &tree) - pub fn mint(mint_signer: Pubkey) -> Self { - Self::Mint(mint_signer) - } - - /// Derive the compressed address. - fn derive_address(&self, address_tree: &Pubkey, program_id: &Pubkey) -> [u8; 32] { - match self { - Self::Pda(pda) => light_compressed_account::address::derive_address( - &pda.to_bytes(), - &address_tree.to_bytes(), - &program_id.to_bytes(), - ), - Self::PdaWithOwner { pda, owner } => light_compressed_account::address::derive_address( - &pda.to_bytes(), - &address_tree.to_bytes(), - &owner.to_bytes(), - ), - Self::Mint(signer) => derive_cmint_compressed_address(signer, address_tree), - } - } -} -``` - -## Result Type - -```rust -/// Proof data for instruction params. Pass directly to instruction data. -/// All accounts use the same address tree, so only one address_tree_info is needed. -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct CreateAccountsProof { - /// The validity proof. - pub proof: ValidityProof, - /// Single packed address tree info (all accounts use same tree). - pub address_tree_info: PackedAddressTreeInfo, - /// Output state tree index for new compressed accounts. - pub output_state_tree_index: u8, -} - -/// Result of get_create_accounts_proof. -pub struct CreateAccountsProofResult { - /// Proof data to include in instruction data. - pub create_accounts_proof: CreateAccountsProof, - /// Remaining accounts to append to instruction accounts. - pub remaining_accounts: Vec, -} -``` - -## Main Function - -````rust -/// Gets validity proof for creating new compressed accounts (INIT flow). -/// -/// Opinionated helper that: -/// - Uses a single address tree (V2) for all addresses -/// - Handles address derivation internally based on input type -/// - Packs proof into remaining accounts -/// -/// # Arguments -/// * `rpc` - RPC client implementing Rpc + Indexer traits -/// * `program_id` - Your program's ID (used as default owner for Pda inputs + system config) -/// * `inputs` - Vec of CreateAccountsProofInput describing accounts to create -/// -/// # Returns -/// CreateAccountsProofResult containing proof, packed tree infos, and remaining accounts. -/// -/// # Example -/// ```rust,ignore -/// let result = get_create_accounts_proof( -/// &rpc, -/// &program_id, -/// vec![ -/// CreateAccountsProofInput::pda(user_pda), -/// CreateAccountsProofInput::pda(game_pda), -/// CreateAccountsProofInput::mint(mint_signer_pda), -/// ], -/// ).await?; -/// -/// // Just pass create_accounts_proof to instruction - macros use defaults -/// let ix = Instruction { -/// program_id, -/// accounts: [my_accounts.to_account_metas(None), result.remaining_accounts].concat(), -/// data: MyInstruction { -/// create_accounts_proof: result.create_accounts_proof, -/// // ... other params -/// }.data(), -/// }; -/// ``` -pub async fn get_create_accounts_proof( - rpc: &R, - program_id: &Pubkey, - inputs: Vec, -) -> Result { - if inputs.is_empty() { - return Err(CreateAccountsProofError::EmptyInputs); - } - - // 1. Get address tree (opinionated: always V2) - let address_tree = rpc.get_address_tree_v2(); - let address_tree_pubkey = address_tree.tree; - - // 2. Derive all compressed addresses (program_id used as default owner for Pda) - let derived_addresses: Vec<[u8; 32]> = inputs - .iter() - .map(|input| input.derive_address(&address_tree_pubkey, program_id)) - .collect(); - - // 3. Build AddressWithTree for each (all use same tree) - let addresses_with_trees: Vec = derived_addresses - .iter() - .map(|&address| AddressWithTree { - address, - tree: address_tree_pubkey, - }) - .collect(); - - // 4. Get validity proof (empty hashes = INIT flow) - let validity_proof = rpc - .get_validity_proof(vec![], addresses_with_trees, None) - .await? - .value; - - // 5. Get output state tree - let state_tree_info = rpc - .get_random_state_tree_info() - .map_err(CreateAccountsProofError::Rpc)?; - - // 6. Determine CPI context - // For INIT with mints: need CPI context for cross-program invocation - let has_mints = inputs.iter().any(|i| matches!(i, CreateAccountsProofInput::Mint(_))); - let cpi_context = if has_mints { - state_tree_info.cpi_context - } else { - None - }; - - // 7. Pack proof - let packed = pack_proof(program_id, validity_proof.clone(), &state_tree_info, cpi_context)?; - - // All addresses use the same tree, so just take the first packed info - let address_tree_info = packed - .packed_tree_infos - .address_trees - .first() - .copied() - .ok_or(CreateAccountsProofError::EmptyInputs)?; - - Ok(CreateAccountsProofResult { - create_accounts_proof: CreateAccountsProof { - proof: validity_proof.proof, - address_tree_info, - output_state_tree_index: packed.output_tree_index, - }, - remaining_accounts: packed.remaining_accounts, - }) -} -```` - -## Error Type - -```rust -#[derive(Debug, Error)] -pub enum CreateAccountsProofError { - #[error("Inputs cannot be empty")] - EmptyInputs, - - #[error("Indexer error: {0}")] - Indexer(#[from] IndexerError), - - #[error("RPC error: {0}")] - Rpc(#[from] RpcError), - - #[error("Pack error: {0}")] - Pack(#[from] PackError), -} -``` - -## Usage Comparison - -### Before (30+ lines) - -```rust -let address_tree_pubkey = rpc.get_address_tree_v2().tree; -let state_tree_info = rpc.get_random_state_tree_info().unwrap(); - -let user_address = derive_address(&user_pda.to_bytes(), &address_tree_pubkey.to_bytes(), &program_id.to_bytes()); -let game_address = derive_address(&game_pda.to_bytes(), &address_tree_pubkey.to_bytes(), &program_id.to_bytes()); -let mint_address = derive_cmint_compressed_address(&mint_signer_pda, &address_tree_pubkey); - -let rpc_result = rpc.get_validity_proof( - vec![], - vec![ - AddressWithTree { address: user_address, tree: address_tree_pubkey }, - AddressWithTree { address: game_address, tree: address_tree_pubkey }, - AddressWithTree { address: mint_address, tree: address_tree_pubkey }, - ], - None, -).await?.value; - -let packed = pack_proof(&program_id, rpc_result.clone(), &state_tree_info, state_tree_info.cpi_context)?; - -let instruction_data = MyInstruction { - proof: rpc_result.proof, - user_address_tree_info: packed.packed_tree_infos.address_trees[0], - game_address_tree_info: packed.packed_tree_infos.address_trees[1], - mint_address_tree_info: packed.packed_tree_infos.address_trees[2], - output_state_tree_index: packed.output_tree_index, - // ... -}; - -let instruction = Instruction { - program_id, - accounts: [accounts.to_account_metas(None), packed.remaining_accounts].concat(), - data: instruction_data.data(), -}; -``` - -### After (5 lines proof setup) - -```rust -let result = get_create_accounts_proof( - &rpc, - &program_id, - vec![ - CreateAccountsProofInput::pda(user_pda), - CreateAccountsProofInput::pda(game_pda), - CreateAccountsProofInput::mint(mint_signer_pda), - ], -).await?; - -// Just pass create_accounts_proof - macros default to params.create_accounts_proof.* -let instruction_data = MyInstruction { - create_accounts_proof: result.create_accounts_proof, - // ... other app-specific params -}; - -let instruction = Instruction { - program_id, - accounts: [accounts.to_account_metas(None), result.remaining_accounts].concat(), - data: instruction_data.data(), -}; -``` - -## Design Decisions - -| Decision | Rationale | -| ------------------------------ | --------------------------------------------------------------------------------- | -| Single address tree | INIT flow always uses V2 address tree; simplifies API | -| Single `address_tree_info` | All accounts use same tree, so one info suffices; macros use this as default | -| Derivation inside helper | Removes error-prone manual derivation | -| `program_id` as default owner | Most PDAs belong to calling program; avoids redundant param per-input | -| `pda_with_owner` escape hatch | Rare case: cross-program accounts need explicit owner | -| CPI context auto-detection | When mints present, automatically includes CPI context | -| Nested result struct | `create_accounts_proof` = instruction data, `remaining_accounts` = accounts | -| Macro defaults to proof fields | `#[compressible]` and `#[light_mint]` default to `params.create_accounts_proof.*` | - -## File Location - -| File | Contents | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | -| `src/create_accounts_proof.rs` | Main implementation | -| `src/lib.rs` | Re-export `get_create_accounts_proof`, `CreateAccountsProofInput`, `CreateAccountsProof`, `CreateAccountsProofResult`, `CreateAccountsProofError` | - -## Dependencies - -```rust -use light_client::indexer::{Indexer, IndexerError, AddressWithTree}; -use light_client::rpc::{Rpc, RpcError}; -use light_token_sdk::compressed_token::create_compressed_mint::derive_cmint_compressed_address; -use light_compressed_account::address::derive_address; -use light_sdk::instruction::{PackedAddressTreeInfo, ValidityProof}; -use crate::pack::{pack_proof, PackError}; -``` diff --git a/sdk-libs/compressible-client/src/create_accounts_proof.rs b/sdk-libs/compressible-client/src/create_accounts_proof.rs index 81e04faa71..7019a7871d 100644 --- a/sdk-libs/compressible-client/src/create_accounts_proof.rs +++ b/sdk-libs/compressible-client/src/create_accounts_proof.rs @@ -6,8 +6,10 @@ //! - Packs proof into remaining accounts //! - Returns a single `address_tree_info` since all accounts use the same tree -use light_client::indexer::{AddressWithTree, Indexer, IndexerError}; -use light_client::rpc::{Rpc, RpcError}; +use light_client::{ + indexer::{AddressWithTree, Indexer, IndexerError}, + rpc::{Rpc, RpcError}, +}; use light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address; use solana_instruction::AccountMeta; use solana_pubkey::Pubkey; diff --git a/sdk-libs/compressible-client/src/decompress_atas.rs b/sdk-libs/compressible-client/src/decompress_atas.rs index 623c58c2a1..300ea73afd 100644 --- a/sdk-libs/compressible-client/src/decompress_atas.rs +++ b/sdk-libs/compressible-client/src/decompress_atas.rs @@ -36,9 +36,8 @@ use light_token_interface::{ }, state::{ExtensionStruct, TokenDataVersion}, }; -use light_token_sdk::compat::TokenData; use light_token_sdk::{ - compat::AccountState, + compat::{AccountState, TokenData}, compressed_token::{ transfer2::{ create_transfer2_instruction, Transfer2AccountsMetaConfig, Transfer2Config, @@ -179,7 +178,7 @@ impl TokenAccountInterface { /// Convenience: get state. #[inline] pub fn state(&self) -> AccountState { - self.token_data.state.clone() + self.token_data.state } /// Returns the compressed account hash if cold (for validity proof). @@ -307,7 +306,7 @@ impl AtaInterface { #[inline] pub fn state(&self) -> AccountState { - self.token_data.state.clone() + self.token_data.state } } diff --git a/sdk-libs/compressible-client/src/decompress_mint.rs b/sdk-libs/compressible-client/src/decompress_mint.rs index 12be840662..42bc878653 100644 --- a/sdk-libs/compressible-client/src/decompress_mint.rs +++ b/sdk-libs/compressible-client/src/decompress_mint.rs @@ -20,8 +20,10 @@ use light_token_interface::{ state::CompressedMint, CMINT_ADDRESS_TREE, }; -use light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address; -use light_token_sdk::token::{find_mint_address, DecompressMint}; +use light_token_sdk::{ + compressed_token::create_compressed_mint::derive_mint_compressed_address, + token::{find_mint_address, DecompressMint}, +}; use solana_account::Account; use solana_instruction::Instruction; use solana_program_error::ProgramError; @@ -49,6 +51,7 @@ pub enum DecompressMintError { /// State of a CMint - either on-chain (hot), compressed (cold), or non-existent. #[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] pub enum MintState { /// CMint exists on-chain - no decompression needed. Hot { account: Account }, diff --git a/sdk-libs/compressible-client/src/initialize_config.rs b/sdk-libs/compressible-client/src/initialize_config.rs index 15915c8cac..97a0756c0b 100644 --- a/sdk-libs/compressible-client/src/initialize_config.rs +++ b/sdk-libs/compressible-client/src/initialize_config.rs @@ -123,10 +123,10 @@ impl InitializeRentFreeConfig { let (config_pda, _) = CompressibleConfig::derive_pda(&self.program_id, self.config_bump); let accounts = vec![ - AccountMeta::new(self.fee_payer, true), // payer - AccountMeta::new(config_pda, false), // config + AccountMeta::new(self.fee_payer, true), // payer + AccountMeta::new(config_pda, false), // config AccountMeta::new_readonly(self.program_data_pda, false), // program_data - AccountMeta::new_readonly(authority, true), // authority + AccountMeta::new_readonly(authority, true), // authority AccountMeta::new_readonly( solana_pubkey::pubkey!("11111111111111111111111111111111"), false, diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs index b124541272..ebe3cb0f8f 100644 --- a/sdk-libs/compressible-client/src/lib.rs +++ b/sdk-libs/compressible-client/src/lib.rs @@ -5,6 +5,10 @@ pub mod get_compressible_account; pub mod initialize_config; pub mod pack; +#[cfg(feature = "anchor")] +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; pub use create_accounts_proof::{ get_create_accounts_proof, CreateAccountsProofError, CreateAccountsProofInput, CreateAccountsProofResult, @@ -27,7 +31,6 @@ pub use decompress_atas::{ DecompressionContext, TokenAccountInterface, }; -pub use light_compressible::CreateAccountsProof; // Re-export TokenData for convenience (standard SPL-compatible type) pub use decompress_mint::{ build_decompress_mint, create_mint_interface, decompress_mint, decompress_mint_idempotent, @@ -35,14 +38,8 @@ pub use decompress_mint::{ DEFAULT_WRITE_TOP_UP, }; pub use initialize_config::InitializeRentFreeConfig; -pub use light_token_sdk::compat::TokenData; -pub use pack::{pack_proof, PackError, PackedProofResult}; - -#[cfg(feature = "anchor")] -use anchor_lang::{AnchorDeserialize, AnchorSerialize}; -#[cfg(not(feature = "anchor"))] -use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; use light_client::indexer::{CompressedAccount, TreeInfo, ValidityProofWithContext}; +pub use light_compressible::CreateAccountsProof; pub use light_sdk::compressible::config::CompressibleConfig; use light_sdk::{ compressible::{compression_info::CompressedAccountData, Pack}, @@ -51,9 +48,11 @@ use light_sdk::{ SystemAccountMetaConfig, ValidityProof, }, }; +pub use light_token_sdk::compat::TokenData; use light_token_sdk::token::{ COMPRESSIBLE_CONFIG_V1, LIGHT_TOKEN_CPI_AUTHORITY, LIGHT_TOKEN_PROGRAM_ID, RENT_SPONSOR, }; +pub use pack::{pack_proof, PackError, PackedProofResult}; use solana_account::Account; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; @@ -393,7 +392,7 @@ pub mod compressible_instruction { let mut has_tokens = false; let mut has_pdas = false; for (compressed_account, _) in compressed_accounts.iter() { - if compressed_account.owner == LIGHT_TOKEN_PROGRAM_ID.into() { + if compressed_account.owner == LIGHT_TOKEN_PROGRAM_ID { has_tokens = true; } else { has_pdas = true; @@ -417,7 +416,7 @@ pub mod compressible_instruction { // Find the first token account's CPI context let first_token_cpi_context = compressed_accounts .iter() - .find(|(acc, _)| acc.owner == LIGHT_TOKEN_PROGRAM_ID.into()) + .find(|(acc, _)| acc.owner == LIGHT_TOKEN_PROGRAM_ID) .map(|(acc, _)| acc.tree_info.cpi_context.unwrap()) .expect("has_tokens is true so there must be a token"); let system_config = diff --git a/sdk-libs/compressible-client/src/pack.rs b/sdk-libs/compressible-client/src/pack.rs index ac3fffdeab..180a28462e 100644 --- a/sdk-libs/compressible-client/src/pack.rs +++ b/sdk-libs/compressible-client/src/pack.rs @@ -23,12 +23,11 @@ use light_client::indexer::{TreeInfo, ValidityProofWithContext}; use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; +pub use light_sdk::instruction::{PackedAddressTreeInfo, PackedStateTreeInfo}; use solana_instruction::AccountMeta; use solana_pubkey::Pubkey; use thiserror::Error; -pub use light_sdk::instruction::{PackedAddressTreeInfo, PackedStateTreeInfo}; - #[derive(Debug, Error)] pub enum PackError { #[error("Failed to add system accounts: {0}")] @@ -103,10 +102,12 @@ pub fn pack_proof( // Convert from light_client's types to our local types let packed_tree_infos = PackedTreeInfos { - state_trees: client_packed_tree_infos.state_trees.map(|st| PackedStateTreeInfos { - packed_tree_infos: st.packed_tree_infos, - output_tree_index: st.output_tree_index, - }), + state_trees: client_packed_tree_infos + .state_trees + .map(|st| PackedStateTreeInfos { + packed_tree_infos: st.packed_tree_infos, + output_tree_index: st.output_tree_index, + }), address_trees: client_packed_tree_infos.address_trees, }; diff --git a/sdk-libs/compressible-client/wrapper.md b/sdk-libs/compressible-client/wrapper.md deleted file mode 100644 index a9f21ffc02..0000000000 --- a/sdk-libs/compressible-client/wrapper.md +++ /dev/null @@ -1,1013 +0,0 @@ -# Unified Decompression Wrapper Specification - -## Problem Statement - -Clients need a single entry point to decompress mixed account types: - -- **ATAs** (Associated Token Accounts) - compression_only tokens owned by ATA pubkey -- **Program-owned CTokens** - tokens owned by program PDAs -- **Program-owned PDAs** - compressed program state - -Each type has different invocation patterns: -| Type | Invocation | Signer | Program Required | -|------|-----------|--------|------------------| -| ATA | Direct invoke to ctoken | wallet_owner | No | -| Program CToken | CPI from user program | program PDA | Yes | -| Program PDA | CPI from user program | program PDA | Yes | - -## Decision Tree - -``` - ┌──────────────────────────────────────┐ - │ What type of compressed account? │ - └───────────────────┬──────────────────┘ - │ - ┌───────────────────────────────┼───────────────────────────────┐ - │ │ │ - ▼ ▼ ▼ - ┌───────────────┐ ┌─────────────────┐ ┌──────────────────┐ - │ ATA │ │ Program PDA │ │ Program CToken │ - │ (wallet owns │ │ (program owns │ │ (program PDA │ - │ the tokens) │ │ the state) │ │ owns tokens) │ - └───────┬───────┘ └────────┬────────┘ └────────┬─────────┘ - │ │ │ - ▼ └──────────────┬───────────────┘ - ┌───────────────┐ │ - │ SDK-ONLY │ ▼ - │ │ ┌───────────────────────┐ - │ decompress_ │ │ REQUIRES PROGRAM │ - │ atas_ │ │ │ - │ idempotent() │ │ User must have on- │ - │ │ │ chain program with │ - │ - No program │ │ decompress_accounts_ │ - │ deployment │ │ idempotent handler │ - │ - Wallet │ │ │ - │ signs │ │ - Program CPI │ - │ - Direct │ │ - Program signs │ - │ invoke │ │ - Needs T: Pack type │ - └───────────────┘ └───────────────────────┘ -``` - -## System Architecture - -``` - ┌─────────────────────────────────┐ - │ decompress_all() │ - │ Unified Entry Point │ - └───────────────┬─────────────────┘ - │ - ┌───────────────┼───────────────┐ - ▼ ▼ ▼ - ┌──────────┐ ┌──────────────┐ ┌──────────┐ - │ ATAs │ │ Program PDAs │ │ Program │ - │ │ │ │ │ CTokens │ - └────┬─────┘ └──────┬───────┘ └────┬─────┘ - │ │ │ - ▼ └───────┬────────┘ - ┌─────────────────┐ │ - │ decompress_atas │ ▼ - │ _idempotent() │ ┌─────────────────────────┐ - │ │ │ decompress_accounts │ - │ Direct invoke │ │ _idempotent() │ - │ to ctoken │ │ │ - └────────┬────────┘ │ CPI through user │ - │ │ program (requires │ - │ │ program_id + discrim) │ - ▼ └────────────┬────────────┘ - ┌─────────────────┐ │ - │ Transaction 1 │ ▼ - │ (SDK-only) │ ┌─────────────────────────┐ - │ │ │ Transaction 2 │ - │ create_ata... │ │ (User Program CPI) │ - │ decompress_ata │ │ │ - └─────────────────┘ │ decompress_pdas+tokens │ - └─────────────────────────┘ -``` - -## Data Flow - -``` -┌──────────────────────────────────────────────────────────────────────────┐ -│ Client Input │ -│ DecompressRequest { │ -│ kind: AccountKind, // ATA | ProgramPda | ProgramCtoken │ -│ pubkey: Pubkey, // The account identifier │ -│ hash: Option<[u8;32]> // Optional: specific compressed hash │ -│ } │ -└─────────────────────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ Phase 1: Classification │ -│ │ -│ for request in requests: │ -│ match request.kind { │ -│ ATA { wallet_owner, mint } => ata_requests.push(...) │ -│ ProgramPda { program_id, seeds } => pda_requests.push(...) │ -│ ProgramCtoken { program_id, seeds } => ctoken_requests.push(...) │ -│ } │ -└─────────────────────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ Phase 2: Query Indexer │ -│ │ -│ ATAs: │ -│ indexer.get_compressed_token_accounts_by_owner(ata_pubkey, mint) │ -│ │ -│ Program PDAs: │ -│ indexer.get_compressed_account_by_address(derived_address) │ -│ │ -│ Program CTokens: │ -│ indexer.get_compressed_token_accounts_by_owner(pda_pubkey, mint) │ -└─────────────────────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ Phase 3: Proof Generation │ -│ │ -│ ATA hashes -> get_validity_proof() -> ata_proof │ -│ PDA + CToken hashes -> get_validity_proof() -> program_proof │ -│ │ -│ Note: PDAs and CTokens share a proof because they're batched in │ -│ the same CPI call through the user program │ -└─────────────────────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ Phase 4: Instruction Building │ -│ │ -│ let mut instructions = Vec::new(); │ -│ │ -│ // ATAs: SDK-only, no program involvement │ -│ if !ata_requests.is_empty() { │ -│ instructions.extend(decompress_atas_idempotent(...)?); │ -│ } │ -│ │ -│ // Program accounts: requires CPI through user program │ -│ if !pda_requests.is_empty() || !ctoken_requests.is_empty() { │ -│ let ix = decompress_accounts_idempotent( │ -│ program_id, │ -│ discriminator, // User provides this │ -│ ... │ -│ )?; │ -│ instructions.push(ix); │ -│ } │ -└─────────────────────────────────┬────────────────────────────────────────┘ - │ - ▼ -┌──────────────────────────────────────────────────────────────────────────┐ -│ Output │ -│ │ -│ DecompressResult { │ -│ ata_instructions: Vec, // Can be sent standalone │ -│ program_instructions: Vec, // Requires user program │ -│ } │ -│ │ -│ OR │ -│ │ -│ Vec where each batch can be a single tx │ -└──────────────────────────────────────────────────────────────────────────┘ -``` - -## API Design - -### Account Kind Enum - -```rust -/// Identifies the type of compressed account for decompression -#[derive(Debug, Clone)] -pub enum AccountKind { - /// ATA-owned compressed token (compression_only) - /// Owner is the ATA pubkey derived from wallet_owner + mint - /// Decompression is SDK-only (direct invoke to ctoken program) - Ata { - wallet_owner: Pubkey, - mint: Pubkey, - }, - - /// Program-owned compressed PDA - /// Requires CPI through user's program - ProgramPda { - /// The program that owns this account - program_id: Pubkey, - /// The PDA pubkey (destination for decompression) - pda_pubkey: Pubkey, - }, - - /// Program-owned compressed token - /// Owner is a PDA of user's program - /// Requires CPI through user's program - ProgramCtoken { - /// The program that owns the PDA which owns the ctoken - program_id: Pubkey, - /// The PDA pubkey that owns the compressed token - owner_pda: Pubkey, - /// The token mint - mint: Pubkey, - }, -} -``` - -### Request Structure - -```rust -/// A request to decompress a specific compressed account -#[derive(Debug, Clone)] -pub struct DecompressRequest { - /// The kind of account and its identifying information - pub kind: AccountKind, - - /// Optional: specific compressed account hash(es) to decompress - /// If None, decompresses ALL compressed accounts matching the kind - pub hashes: Option>, -} -``` - -### Program Config (for PDA/CToken operations) - -```rust -/// Configuration for program-owned account decompression -/// Only needed if decompressing ProgramPda or ProgramCtoken accounts -#[derive(Debug, Clone)] -pub struct ProgramDecompressConfig { - /// The program ID that owns the accounts - pub program_id: Pubkey, - - /// The discriminator for decompress_accounts_idempotent instruction - /// SHA256("global:decompress_accounts_idempotent")[..8] - pub discriminator: [u8; 8], - - /// Account metas for the program's DecompressAccountsIdempotent accounts struct - /// This is program-specific and must be provided by the client - pub program_account_metas: Vec, - - /// Packed account data type deserializer - /// Used to convert compressed account data to the program's variant type - pub pack_fn: fn(&CompressedAccount) -> Result, -} -``` - -### Result Structure - -```rust -/// Result of decompress_all operation -#[derive(Debug)] -pub struct DecompressResult { - /// Instructions for ATA decompression (SDK-only, no program needed) - /// These can be sent as a standalone transaction - pub ata_instructions: Vec, - - /// Instructions for program-owned account decompression - /// These require the user's program to be deployed - /// Each inner Vec is a set of instructions that must go in the same tx - pub program_instructions: Vec>, - - /// Accounts that were skipped (already decompressed or not found) - pub skipped: Vec, -} - -#[derive(Debug)] -pub struct SkippedAccount { - pub kind: AccountKind, - pub reason: SkipReason, -} - -#[derive(Debug)] -pub enum SkipReason { - NotFound, - AlreadyDecompressed, - InvalidState, -} -``` - -### Main Entry Point - -````rust -/// Unified decompression entry point -/// -/// Given a list of accounts to decompress (with their kinds), generates -/// the appropriate instructions to decompress them. -/// -/// # Arguments -/// * `requests` - List of decompression requests -/// * `fee_payer` - The fee payer for all transactions -/// * `program_config` - Required if any ProgramPda or ProgramCtoken requests exist -/// * `indexer` - Indexer for querying compressed state and proofs -/// -/// # Returns -/// * `DecompressResult` containing categorized instructions -/// -/// # Example -/// ```rust -/// let requests = vec![ -/// DecompressRequest { -/// kind: AccountKind::Ata { wallet_owner, mint }, -/// hashes: None, // decompress all -/// }, -/// DecompressRequest { -/// kind: AccountKind::ProgramPda { program_id, pda_pubkey }, -/// hashes: Some(vec![specific_hash]), -/// }, -/// ]; -/// -/// let result = decompress_all( -/// &requests, -/// fee_payer, -/// Some(program_config), // needed for ProgramPda -/// &indexer, -/// ).await?; -/// -/// // Send ATA instructions first (no dependencies) -/// rpc.send_transaction(result.ata_instructions); -/// -/// // Send program instructions (requires user program) -/// for ix_batch in result.program_instructions { -/// rpc.send_transaction(ix_batch); -/// } -/// ``` -pub async fn decompress_all( - requests: &[DecompressRequest], - fee_payer: Pubkey, - program_config: Option<&ProgramDecompressConfig>, - indexer: &I, -) -> Result -```` - -### Convenience Functions - -```rust -/// Decompress only ATAs (simplified API for common case) -pub async fn decompress_only_atas( - wallet_mints: &[(Pubkey, Pubkey)], // (wallet_owner, mint) pairs - fee_payer: Pubkey, - indexer: &I, -) -> Result, DecompressError> { - let requests: Vec<_> = wallet_mints - .iter() - .map(|(wallet_owner, mint)| DecompressRequest { - kind: AccountKind::Ata { - wallet_owner: *wallet_owner, - mint: *mint, - }, - hashes: None, - }) - .collect(); - - let result = decompress_all(&requests, fee_payer, None, indexer).await?; - Ok(result.ata_instructions) -} - -/// Decompress program accounts with a pre-built config -pub async fn decompress_program_accounts( - program_id: &Pubkey, - discriminator: &[u8; 8], - pda_pubkeys: &[Pubkey], - program_account_metas: Vec, - fee_payer: Pubkey, - indexer: &I, -) -> Result -``` - -## Implementation Plan - -### Phase 1: Core Types (wrapper_types.rs) - -1. `AccountKind` enum -2. `DecompressRequest` struct -3. `ProgramDecompressConfig` struct -4. `DecompressResult` struct -5. `DecompressError` error enum - -### Phase 2: Request Classification (wrapper.rs) - -1. `classify_requests()` - separates ATAs from program accounts -2. `validate_program_config()` - ensures config exists when needed - -### Phase 3: Indexer Queries - -1. Batch ATA queries by wallet_owner -2. Batch PDA queries by program_id -3. Batch CToken queries by owner_pda - -### Phase 4: Proof Generation - -1. Collect all hashes per category -2. `get_validity_proof()` for ATA hashes -3. `get_validity_proof()` for program account hashes - -### Phase 5: Instruction Building - -1. Call existing `decompress_atas_idempotent()` for ATAs -2. Call existing `decompress_accounts_idempotent()` for program accounts - -## Transaction Batching Considerations - -``` -┌────────────────────────────────────────────────────────────────────────┐ -│ Transaction Batching Rules │ -│ │ -│ 1. ATAs: All in same tx (or split by compute limit) │ -│ - create_ata_idempotent... (multiple) │ -│ - decompress_batch (single ix, multiple inputs) │ -│ │ -│ 2. Program PDAs + CTokens: All in same tx if same program │ -│ - decompress_accounts_idempotent(pda1, pda2, ctoken1, ctoken2) │ -│ - Order: PDAs first, then CTokens (CPI context handling) │ -│ │ -│ 3. Mixed programs: Separate transactions │ -│ - Each program_id gets its own decompress_accounts_idempotent │ -│ │ -│ 4. Compute limits: May need to split large batches │ -│ - ~200k CU per decompression │ -│ - ~1.4M CU limit per tx │ -│ - Max ~7 decompressions per tx │ -└────────────────────────────────────────────────────────────────────────┘ -``` - -## Error Handling - -```rust -#[derive(Debug, Error)] -pub enum DecompressError { - #[error("Indexer error: {0}")] - Indexer(#[from] IndexerError), - - #[error("CToken SDK error: {0}")] - CTokenSdk(#[from] CTokenSdkError), - - #[error("Program config required for ProgramPda or ProgramCtoken accounts")] - ProgramConfigRequired, - - #[error("Program ID mismatch: expected {expected}, got {got}")] - ProgramIdMismatch { expected: Pubkey, got: Pubkey }, - - #[error("No compressed accounts found for any request")] - NoAccountsFound, - - #[error("Instruction building failed: {0}")] - InstructionBuild(String), -} -``` - -## Usage Examples - -### Example 1: Decompress User's ATAs Only - -```rust -// User wants to decompress all their compressed USDC and SOL tokens -let result = decompress_all( - &[ - DecompressRequest { - kind: AccountKind::Ata { - wallet_owner: user_wallet, - mint: usdc_mint - }, - hashes: None, - }, - DecompressRequest { - kind: AccountKind::Ata { - wallet_owner: user_wallet, - mint: wsol_mint - }, - hashes: None, - }, - ], - user_wallet, // fee payer - None, // no program config needed - &indexer, -).await?; - -// Send single transaction -send_transaction(result.ata_instructions).await?; -``` - -### Example 2: Decompress Game State (Mixed) - -```rust -// Game has: user PDA (score), reward tokens (program-owned) -let program_config = ProgramDecompressConfig { - program_id: game_program_id, - discriminator: game::instruction::DecompressAccountsIdempotent::DISCRIMINATOR, - program_account_metas: game::accounts::DecompressAccountsIdempotent { - fee_payer: user_wallet, - config: config_pda, - rent_sponsor: rent_sponsor, - // ... other accounts - }.to_account_metas(None), - pack_fn: |acc| GameAccountVariant::try_from(acc), -}; - -let result = decompress_all( - &[ - // User's ATA (their own tokens) - DecompressRequest { - kind: AccountKind::Ata { - wallet_owner: user_wallet, - mint: reward_mint - }, - hashes: None, - }, - // Game PDA (score state) - DecompressRequest { - kind: AccountKind::ProgramPda { - program_id: game_program_id, - pda_pubkey: score_pda, - }, - hashes: None, - }, - // Program-owned reward tokens - DecompressRequest { - kind: AccountKind::ProgramCtoken { - program_id: game_program_id, - owner_pda: reward_vault_pda, - mint: reward_mint, - }, - hashes: None, - }, - ], - user_wallet, - Some(&program_config), - &indexer, -).await?; - -// Transaction 1: ATAs (no program needed) -send_transaction(result.ata_instructions).await?; - -// Transaction 2: Game state + program tokens (needs game program) -for ix_batch in result.program_instructions { - send_transaction(ix_batch).await?; -} -``` - -## Key Constraints - -1. **ATAs are SDK-only**: No program deployment needed, wallet signs directly -2. **Program accounts need CPI**: User must have deployed program with `decompress_accounts_idempotent` -3. **Proof batching**: Single proof for multiple accounts of same category -4. **CPI context ordering**: When mixing PDAs + CTokens, PDAs write first, CTokens consume last -5. **Program ID grouping**: Different programs = different transactions - -## Files to Create/Modify - -1. `sdk-libs/compressible-client/src/wrapper_types.rs` - Type definitions -2. `sdk-libs/compressible-client/src/wrapper.rs` - Main implementation -3. `sdk-libs/compressible-client/src/lib.rs` - Export new modules - -## Dependencies - -- Existing: `decompress_atas_idempotent` (just implemented) -- Existing: `compressible_instruction::decompress_accounts_idempotent` -- Existing: `light_client::indexer::Indexer` trait -- Existing: `light_sdk::compressible::Pack` trait - ---- - -## Design Alternative: Simpler Approach - -The main complexity in the unified wrapper comes from handling the generic `T: Pack` constraint for program-owned accounts. Each program defines its own `CompressedAccountVariant` enum. - -### Alternative: Two-Tier API - -Instead of one function that does everything, provide: - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Two-Tier API │ -│ │ -│ Tier 1: SDK-Only (no user program needed) │ -│ ───────────────────────────────────────── │ -│ decompress_atas_idempotent() - Already implemented │ -│ │ -│ Tier 2: Program-Aware (requires user program types) │ -│ ─────────────────────────────────────────────────── │ -│ decompress_program_accounts() - Generic over program variant │ -│ │ -│ Combination Helper (convenience, not generic) │ -│ ───────────────────────────────────────────── │ -│ DecompressBuilder - Builder pattern for combining requests │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### Builder Pattern API - -```rust -/// Builder for constructing mixed decompression requests -pub struct DecompressBuilder<'a, I: Indexer> { - indexer: &'a I, - fee_payer: Pubkey, - ata_requests: Vec, - /// Program requests stored as pre-built instructions (caller handles T: Pack) - program_instructions: Vec, -} - -impl<'a, I: Indexer> DecompressBuilder<'a, I> { - pub fn new(indexer: &'a I, fee_payer: Pubkey) -> Self { - Self { - indexer, - fee_payer, - ata_requests: Vec::new(), - program_instructions: Vec::new(), - } - } - - /// Add an ATA to decompress - pub fn add_ata(mut self, wallet_owner: Pubkey, mint: Pubkey) -> Self { - self.ata_requests.push(DecompressAtaRequest { - wallet_owner, - mint, - hashes: None, - }); - self - } - - /// Add multiple ATAs for same wallet - pub fn add_atas(mut self, wallet_owner: Pubkey, mints: &[Pubkey]) -> Self { - for mint in mints { - self.ata_requests.push(DecompressAtaRequest { - wallet_owner, - mint: *mint, - hashes: None, - }); - } - self - } - - /// Add a pre-built program decompression instruction - /// Caller is responsible for building this with correct T: Pack type - pub fn add_program_instruction(mut self, instruction: Instruction) -> Self { - self.program_instructions.push(instruction); - self - } - - /// Build all instructions - pub async fn build(self) -> Result { - let ata_instructions = if self.ata_requests.is_empty() { - Vec::new() - } else { - decompress_atas_idempotent(&self.ata_requests, self.fee_payer, self.indexer).await? - }; - - Ok(DecompressResult { - ata_instructions, - program_instructions: self.program_instructions, - skipped: Vec::new(), - }) - } -} -``` - -### Usage with Builder - -```rust -// Simple: ATAs only -let result = DecompressBuilder::new(&indexer, fee_payer) - .add_ata(wallet, usdc_mint) - .add_ata(wallet, wsol_mint) - .build() - .await?; - -// Mixed: ATAs + program accounts -// Step 1: User builds their program instruction (they know the types) -let program_ix = compressible_instruction::decompress_accounts_idempotent::( - &game_program_id, - &discriminator, - &[pda1, pda2], - &[(compressed_pda1, variant1), (compressed_pda2, variant2)], - &program_account_metas, - validity_proof, -)?; - -// Step 2: Combine with builder -let result = DecompressBuilder::new(&indexer, fee_payer) - .add_ata(wallet, reward_mint) - .add_program_instruction(program_ix) - .build() - .await?; -``` - -### Why This is Better - -1. **No phantom type complexity**: Caller handles `T: Pack` themselves -2. **No trait objects/dynamic dispatch**: Instructions are concrete -3. **Composable**: Easy to add more instruction types later -4. **Type safe**: Program-specific types stay in caller's code -5. **Simpler implementation**: Builder just aggregates, doesn't transform - ---- - -## Recommended Implementation - -Given the complexity of generic type handling across programs, the recommended implementation is: - -### Core Functions (Already Exist / Just Built) - -1. `decompress_atas_idempotent()` - SDK-only ATA decompression -2. `compressible_instruction::decompress_accounts_idempotent()` - Program account decompression - -### New Functions to Add - -```rust -/// Convenience: Decompress all ATAs for a wallet across multiple mints -pub async fn decompress_wallet_atas( - wallet_owner: Pubkey, - mints: &[Pubkey], - fee_payer: Pubkey, - indexer: &I, -) -> Result, DecompressAtaError> { - let requests: Vec<_> = mints - .iter() - .map(|mint| DecompressAtaRequest { - wallet_owner, - mint: *mint, - hashes: None, - }) - .collect(); - decompress_atas_idempotent(&requests, fee_payer, indexer).await -} - -/// Query helper: Find all compressed accounts for program-owned PDAs -/// Returns data needed to call decompress_accounts_idempotent -pub async fn query_program_compressed_accounts( - program_id: &Pubkey, - pda_pubkeys: &[Pubkey], - indexer: &I, -) -> Result { - // Derives addresses, queries indexer, returns compressed accounts - // Caller then deserializes data into their T type -} - -/// Query helper: Find all compressed tokens owned by program PDAs -pub async fn query_program_compressed_tokens( - owner_pdas: &[Pubkey], - mint: Option, - indexer: &I, -) -> Result, DecompressError> { - // Queries indexer for each owner PDA -} -``` - -### Final Recommended API - -``` -┌──────────────────────────────────────────────────────────────────────────┐ -│ Recommended API Surface │ -│ │ -│ HIGH-LEVEL (SDK-only) │ -│ ───────────────────── │ -│ decompress_atas_idempotent() // Multiple ATAs, one proof │ -│ decompress_wallet_atas() // All ATAs for wallet │ -│ decompress_all_for_ata() // Single ATA convenience │ -│ │ -│ QUERY HELPERS (for program accounts) │ -│ ───────────────────────────────────── │ -│ query_program_compressed_accounts() // Find PDAs, return raw data │ -│ query_program_compressed_tokens() // Find program-owned tokens │ -│ │ -│ LOW-LEVEL (generic, caller provides types) │ -│ ────────────────────────────────────────── │ -│ decompress_accounts_idempotent() // Build instruction │ -│ │ -│ BUILDER (composition) │ -│ ───────────────────── │ -│ DecompressBuilder // Combine ATAs + program ixs │ -└──────────────────────────────────────────────────────────────────────────┘ -``` - -This keeps the API clean while acknowledging that program-specific type handling must stay with the caller. - ---- - -## Complete End-to-End Flow Diagrams - -### Scenario 1: User Decompresses Their Own ATAs (SDK-Only) - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ User: "I want to decompress my USDC and SOL tokens" │ -└──────────────────────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Client Code │ -│ │ -│ let requests = vec![ │ -│ DecompressAtaRequest { wallet_owner, mint: usdc_mint, hashes: None }, │ -│ DecompressAtaRequest { wallet_owner, mint: wsol_mint, hashes: None }, │ -│ ]; │ -│ │ -│ let instructions = decompress_atas_idempotent(&requests, wallet, &ix) │ -│ .await?; │ -└──────────────────────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Internal: decompress_atas_idempotent │ -│ │ -│ 1. Derive ATA pubkeys: (wallet, usdc) -> ata1, (wallet, sol) -> ata2 │ -│ │ -│ 2. Query indexer: │ -│ indexer.get_compressed_token_accounts_by_owner(ata1, usdc) │ -│ indexer.get_compressed_token_accounts_by_owner(ata2, sol) │ -│ │ -│ 3. Get single proof for all hashes: │ -│ indexer.get_validity_proof([hash1, hash2, hash3...]) │ -│ │ -│ 4. Build instructions: │ -│ - create_ata_idempotent(wallet, usdc) │ -│ - create_ata_idempotent(wallet, sol) │ -│ - transfer2(decompress all tokens to ATAs) │ -└──────────────────────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Single Transaction │ -│ │ -│ Instructions: [create_ata_usdc, create_ata_sol, decompress_batch] │ -│ Signers: [wallet] │ -│ │ -│ No on-chain program needed - direct invoke to ctoken program │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### Scenario 2: Game Decompresses Player State (Program Required) - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ User: "I want to decompress my game score PDA and reward tokens" │ -└──────────────────────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Client Code (Game-Specific) │ -│ │ -│ // Step 1: Query compressed accounts │ -│ let score_address = derive_address(&score_pda, &tree, &game_id); │ -│ let score_account = indexer.get_compressed_account(score_address).await?; │ -│ let token_accounts = indexer │ -│ .get_compressed_token_accounts_by_owner(&reward_vault_pda, mint) │ -│ .await?; │ -│ │ -│ // Step 2: Deserialize into game's variant types │ -│ let score_variant = GameVariant::Score( │ -│ ScoreData::deserialize(&score_account.data)? │ -│ ); │ -│ let token_variants: Vec<_> = token_accounts.iter() │ -│ .map(|acc| GameVariant::Token(acc.token.clone())) │ -│ .collect(); │ -│ │ -│ // Step 3: Get proof for all accounts │ -│ let all_hashes = [score_account.hash].iter() │ -│ .chain(token_accounts.iter().map(|a| a.account.hash)) │ -│ .collect(); │ -│ let proof = indexer.get_validity_proof(all_hashes, [], None).await?; │ -│ │ -│ // Step 4: Build program instruction │ -│ let ix = decompress_accounts_idempotent::( │ -│ &game_program_id, │ -│ &game::DECOMPRESS_DISCRIMINATOR, │ -│ &[score_pda, reward_vault_pda], │ -│ &[(score_account, score_variant), ...token_variants], │ -│ &game_account_metas, │ -│ proof, │ -│ )?; │ -└──────────────────────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Single Transaction │ -│ │ -│ Instructions: [decompress_accounts_idempotent] │ -│ Signers: [wallet] │ -│ │ -│ On-chain game program executes: │ -│ 1. CPI to light-system-program (writes PDA to CPI context) │ -│ 2. CPI to ctoken-program (decompresses tokens, consumes CPI context) │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - -### Scenario 3: Mixed ATAs + Program Accounts - -``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ User: "Decompress my personal tokens AND my game state" │ -└──────────────────────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Client Code (Using Builder Pattern) │ -│ │ -│ // Build ATAs │ -│ let ata_ixs = decompress_atas_idempotent(&[ │ -│ DecompressAtaRequest { wallet_owner, mint: personal_token, ... }, │ -│ ], wallet, &indexer).await?; │ -│ │ -│ // Build program instruction (separate, with game types) │ -│ let game_ix = build_game_decompress_ix(...)?; // as shown in Scenario 2 │ -│ │ -│ // Combine using builder │ -│ let result = DecompressBuilder::new(&indexer, wallet) │ -│ .with_ata_instructions(ata_ixs) │ -│ .with_program_instruction(game_ix) │ -│ .build()?; │ -└──────────────────────────────────┬──────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────────┐ -│ Two Transactions (or combined if compute allows) │ -│ │ -│ Transaction 1 (ATAs - SDK only): │ -│ - create_ata_idempotent │ -│ - decompress_atas_batch │ -│ - Signers: [wallet] │ -│ │ -│ Transaction 2 (Game - needs program): │ -│ - decompress_accounts_idempotent │ -│ - Signers: [wallet] │ -│ - Program: game_program handles CPI │ -└─────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Implementation Checklist - -### Already Implemented - -- [x] `decompress_atas_idempotent` - SDK-only ATA decompression -- [x] `decompress_all_for_ata` - Single ATA convenience -- [x] `decompress_multiple_atas` - Simple multi-ATA wrapper -- [x] `compressible_instruction::decompress_accounts_idempotent` - Program accounts - -### To Implement (New Wrapper Layer) - -1. **Query Helpers** (new file: `query_helpers.rs`) - -```rust -// Find compressed PDAs by their derived addresses -pub async fn query_compressed_pdas( - pda_pubkeys: &[Pubkey], - address_tree: &Pubkey, - program_id: &Pubkey, - indexer: &I, -) -> Result, DecompressError>; - -// Find compressed tokens by owner PDAs -pub async fn query_compressed_tokens_by_owners( - owner_pdas: &[Pubkey], - mint: Option, - indexer: &I, -) -> Result)>, DecompressError>; -``` - -2. **DecompressBuilder** (new file: `decompress_builder.rs`) - -```rust -pub struct DecompressBuilder { ... } - -impl DecompressBuilder { - pub fn new(fee_payer: Pubkey) -> Self; - pub fn with_ata_instructions(self, ixs: Vec) -> Self; - pub fn with_program_instruction(self, ix: Instruction) -> Self; - pub fn build(self) -> DecompressResult; -} -``` - -3. **Additional Convenience Functions** (add to `decompress_atas.rs`) - -```rust -// Decompress all ATAs for a wallet -pub async fn decompress_wallet_atas( - wallet: Pubkey, - mints: &[Pubkey], - fee_payer: Pubkey, - indexer: &I, -) -> Result, DecompressAtaError>; -``` - -### Files to Create/Modify - -| File | Action | Contents | -| --------------------------- | ------ | ----------------------------------- | -| `src/query_helpers.rs` | Create | Query utilities for PDAs and tokens | -| `src/decompress_builder.rs` | Create | Builder for combining requests | -| `src/decompress_atas.rs` | Modify | Add `decompress_wallet_atas` | -| `src/lib.rs` | Modify | Export new modules | - ---- - -## Summary - -The recommended approach is a **two-tier API**: - -1. **SDK-only tier** (for ATAs): Fully automatic, no program needed -2. **Program-aware tier**: Caller provides types, we provide query helpers - -The builder pattern bridges both tiers for mixed use cases, keeping type safety while maintaining flexibility. - -Key insight: We cannot fully abstract away the `T: Pack` generic without either: - -- Runtime type erasure (losing type safety) -- Macro-generated code per program (complexity) - -The builder pattern sidesteps this by letting callers handle their own types while we handle the orchestration. diff --git a/sdk-libs/macros/MACRO-NEW.md b/sdk-libs/macros/MACRO-NEW.md deleted file mode 100644 index cf26f68d81..0000000000 --- a/sdk-libs/macros/MACRO-NEW.md +++ /dev/null @@ -1,792 +0,0 @@ -# Compressible Macro - Final Implementation - -## Overview - -The `#[compressible(...)]` macro generates all types and code needed for rent-free account compression/decompression. This document shows exactly what exists now and how to use it. - -## Status: Complete (Phase 1-8) - -All phases implemented and tested, including Phase 8 CToken seed refactor. See `csdk-anchor-full-derived-test` for working example. - -### Phase 8 Key Changes - -- `TokenAccountVariant` now has struct variants with Pubkey fields for seeds -- `PackedTokenAccountVariant` has struct variants with u8 idx fields -- `TokenSeedProvider` trait no longer requires accounts struct parameter -- `DecompressAccountsIdempotent` no longer needs named seed accounts -- All seed resolution happens via variant idx fields and `post_system_accounts` - ---- - -## 1. Macro Declaration - -```rust -#[compressible( - // PDA accounts with seeds - UserRecord = (seeds = ("user_record", ctx.authority, ctx.mint_authority, data.owner, data.category_id.to_le_bytes())), - GameSession = (seeds = (GAME_SESSION_SEED, ctx.user, ctx.authority, data.session_id.to_le_bytes())), - - // Token accounts (CTokens) - Vault = (is_token, seeds = ("vault", ctx.cmint), authority = ("vault_authority")), - - // Instruction data fields (for data.* seeds) - owner = Pubkey, - category_id = u64, - session_id = u64, -)] -pub mod my_program { ... } -``` - -### Seed Types - -- `ctx.*` - Context accounts (Pubkeys from instruction accounts) -- `data.*` - Data fields (from compressed account data, verified at construction time) -- String literals - Static seeds -- Constants - e.g., `GAME_SESSION_SEED` - ---- - -## 2. Generated Types - -### 2.1 CompressedAccountVariant Enum (Struct Variants) - -```rust -pub enum CompressedAccountVariant { - // Unpacked variants (with ctx.* Pubkeys) - UserRecord { - data: UserRecord, - authority: Pubkey, - mint_authority: Pubkey, - }, - GameSession { - data: GameSession, - user: Pubkey, - authority: Pubkey, - }, - - // Packed variants (with u8 indices into remaining_accounts) - PackedUserRecord { - data: PackedUserRecord, - authority_idx: u8, - mint_authority_idx: u8, - }, - PackedGameSession { - data: PackedGameSession, - user_idx: u8, - authority_idx: u8, - }, - - // CToken variant (unchanged) - CTokenData(CTokenData), -} -``` - -### 2.2 Seeds Structs (All Seeds - ctx._ + data._) - -```rust -pub struct UserRecordSeeds { - pub authority: Pubkey, // ctx.authority - pub mint_authority: Pubkey, // ctx.mint_authority - pub owner: Pubkey, // data.owner (verified against account) - pub category_id: u64, // data.category_id (verified against account) -} - -pub struct GameSessionSeeds { - pub user: Pubkey, // ctx.user - pub authority: Pubkey, // ctx.authority - pub session_id: u64, // data.session_id (verified against account) -} -``` - -### 2.3 Constructor Methods - -```rust -impl CompressedAccountVariant { - /// Deserializes data and verifies data.* seeds match. - pub fn user_record( - account_data: &[u8], - seeds: UserRecordSeeds, - ) -> Result { - let data = UserRecord::deserialize(&mut &account_data[..])?; - - // Verify data.* seeds match actual compressed data - if data.owner != seeds.owner { return Err(SeedMismatch); } - if data.category_id != seeds.category_id { return Err(SeedMismatch); } - - Ok(Self::UserRecord { - data, - authority: seeds.authority, - mint_authority: seeds.mint_authority, - }) - } - - pub fn game_session( - account_data: &[u8], - seeds: GameSessionSeeds, - ) -> Result { ... } -} -``` - -### 2.4 SeedParams Removed - -`SeedParams` is no longer needed in instruction data. All seeds are now resolved: - -- `ctx.*` seeds: From variant idx fields → resolved on-chain via `post_system_accounts` -- `data.*` seeds: From unpacked compressed account data (`self.field`) - ---- - -## 3. Client API Types - -### 3.1 AccountInterface - -```rust -pub struct AccountInterface { - pub pubkey: Pubkey, - pub is_cold: bool, - pub decompression_context: Option, -} - -pub struct PdaDecompressionContext { - pub compressed_account: CompressedAccount, -} - -impl AccountInterface { - pub fn cold(pubkey: Pubkey, compressed_account: CompressedAccount) -> Self; - pub fn hot(pubkey: Pubkey) -> Self; - pub fn compressed_data(&self) -> Option<&[u8]>; -} -``` - -### 3.2 RentFreeDecompressAccount - -```rust -pub struct RentFreeDecompressAccount { - pub account_interface: AccountInterface, - pub variant: V, -} -``` - -### 3.3 Instruction Builders - -```rust -// Existing API (still works) -pub fn decompress_accounts_idempotent( - program_id: &Pubkey, - discriminator: &[u8], - decompressed_account_addresses: &[Pubkey], - compressed_accounts: &[(CompressedAccount, T)], - program_account_metas: &[AccountMeta], - validity_proof_with_context: ValidityProofWithContext, -) -> Result - -// New API (filters cold accounts automatically) -pub fn decompress_accounts_idempotent_new( - program_id: &Pubkey, - accounts: Vec>, - program_account_metas: &[AccountMeta], - validity_proof_with_context: ValidityProofWithContext, - discriminator: Option<&[u8]>, -) -> Result, Error> // Returns None if no cold accounts -``` - ---- - -## 4. Client Usage (Complete Example) - -From `csdk-anchor-full-derived-test/tests/basic_test.rs`: - -```rust -use csdk_anchor_full_derived_test::{TokenAccountVariant, CompressedAccountVariant}; -use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ - GameSessionSeeds, UserRecordSeeds, // NO SeedParams needed -}; -use light_compressible_client::{ - compressible_instruction, AccountInterface, RentFreeDecompressAccount, -}; - -// 1. Fetch compressed accounts from indexer -let compressed_user = rpc - .get_compressed_account(user_compressed_address, None) - .await? - .value - .unwrap(); - -let compressed_game = rpc - .get_compressed_account(game_compressed_address, None) - .await? - .value - .unwrap(); - -// 2. Fetch compressed token accounts -let compressed_vault_accounts = rpc - .get_compressed_token_accounts_by_owner(&vault_pda, None, None) - .await? - .value - .items; -let compressed_vault = &compressed_vault_accounts[0]; - -// 3. Get validity proof for all accounts -let rpc_result = rpc - .get_validity_proof( - vec![ - compressed_user.hash, - compressed_game.hash, - compressed_vault.account.hash, - ], - vec![], - None, - ) - .await? - .value; - -// 4. Create AccountInterface for each cold account (from RPC response) -let user_interface = AccountInterface::cold(user_record_pda, compressed_user.clone()); -let game_interface = AccountInterface::cold(game_session_pda, compressed_game.clone()); -let vault_interface = AccountInterface::cold(vault_pda, compressed_vault.account.clone()); - -// 5. Construct variants using generated constructors (verifies data.* seeds match) -let user_variant = CompressedAccountVariant::user_record( - user_interface.compressed_data().unwrap(), - UserRecordSeeds { - authority: authority.pubkey(), - mint_authority: mint_authority.pubkey(), - owner, // Must match compressed data - category_id, // Must match compressed data - }, -).expect("UserRecord seed verification failed"); - -let game_variant = CompressedAccountVariant::game_session( - game_interface.compressed_data().unwrap(), - GameSessionSeeds { - user: payer.pubkey(), - authority: authority.pubkey(), - session_id, // Must match compressed data - }, -).expect("GameSession seed verification failed"); - -let vault_ctoken_data = light_token_sdk::compat::CTokenData { - variant: TokenAccountVariant::Vault, - token_data: compressed_vault.token.clone(), -}; - -// 6. Build RentFreeDecompressAccount for each account -let decompress_accounts = vec![ - RentFreeDecompressAccount::new(user_interface, user_variant), - RentFreeDecompressAccount::new(game_interface, game_variant), - RentFreeDecompressAccount::new( - vault_interface, - CompressedAccountVariant::CTokenData(vault_ctoken_data), - ), -]; - -// 7. Build decompress instruction using NEW API - NO SeedParams or seed accounts needed! -let decompress_instruction = compressible_instruction::decompress_accounts_idempotent_new( - &program_id, - decompress_accounts, - compressible_instruction::decompress::accounts(payer.pubkey(), config_pda, payer.pubkey()), - rpc_result, -)? -.expect("Should have cold accounts to decompress"); - -// 8. Send transaction - done! -rpc.create_and_send_transaction(&[decompress_instruction], &payer.pubkey(), &[&payer]) - .await?; -``` - ---- - -## 5. On-Chain Flow - -### 5.1 What Happens When Instruction Executes - -``` -1. UNPACK - PackedUserRecord { data, authority_idx: 3, mint_authority_idx: 5 } - -> authority = remaining_accounts[3].key - -> mint_authority = remaining_accounts[5].key - -> UserRecord { data, authority, mint_authority } - -2. DERIVE PDA - UserRecordCtxSeeds { authority, mint_authority } - + - self (unpacked UserRecord) - -> seeds = ["user_record", authority, mint_authority, self.owner, self.category_id.to_le_bytes()] - -> derived_pda = Pubkey::find_program_address(&seeds, program_id) - -3. VERIFY - assert!(derived_pda == target_solana_account.key) - -4. CREATE/WRITE - if !account_exists { - create_pda(derived_pda) - } - write_data(data) -``` - -### 5.2 CPI Context Batching (Mixed PDAs + Tokens) - -``` -CRITICAL: When has_pdas && has_tokens: -1. PDAs FIRST: LightSystemProgramCpi.write_to_cpi_context_first() -2. Tokens LAST: invoke() with cpi_context (consumes context) - -Client must use FIRST TOKEN's cpi_context when packing. -``` - ---- - -## 6. Key Implementation Details - -### 6.1 Seed Resolution - -| Seed Type | Where Stored | Resolved At | -| --------- | ---------------------- | ---------------------------------------- | -| `ctx.*` | Variant idx field (u8) | On-chain via `post_system_accounts[idx]` | -| `data.*` | Unpacked account data | On-chain via `self.field` | -| Literals | Hardcoded in macro | Compile time | -| Constants | Hardcoded in macro | Compile time | - -### 6.2 Index Space - -All indices (`authority_idx`, `mint_authority_idx`, etc.) reference `remaining_accounts` after system accounts: - -``` -remaining_accounts layout: -[0..system_end]: System accounts (light_system_program, etc.) -[system_end..tail_start]: Packed pubkeys (deduped) -[tail_start..]: Decompressed PDA addresses -``` - -### 6.3 Pack/Unpack Flow - -```rust -// Client: Pack (Pubkey -> u8) -CompressedAccountVariant::UserRecord { data, authority, mint_authority } --> Pack::pack(&mut remaining_accounts) --> CompressedAccountVariant::PackedUserRecord { - data: data.pack(&mut remaining_accounts), - authority_idx: remaining_accounts.insert_or_get(authority), - mint_authority_idx: remaining_accounts.insert_or_get(mint_authority), - } - -// On-chain: Unpack (u8 -> Pubkey) -PackedUserRecord { data, authority_idx, mint_authority_idx } --> Unpack::unpack(post_system_accounts) --> UserRecord { - data: data.unpack(post_system_accounts)?, - authority: *post_system_accounts[authority_idx].key, - mint_authority: *post_system_accounts[mint_authority_idx].key, - } -``` - ---- - -## 7. Files Reference - -| File | Purpose | -| -------------------------------------------------------- | ------------------------------------------------- | -| `sdk-libs/macros/src/compressible/instructions.rs` | Main macro, generates seeds structs, constructors | -| `sdk-libs/macros/src/compressible/variant_enum.rs` | CompressedAccountVariant enum, Pack/Unpack | -| `sdk-libs/macros/src/compressible/decompress_context.rs` | DecompressContext trait impl | -| `sdk-libs/macros/src/compressible/seed_providers.rs` | CToken seed provider (unchanged) | -| `sdk-libs/compressible-client/src/lib.rs` | Client API types and instruction builders | -| `sdk-tests/csdk-anchor-full-derived-test/` | Complete working example | - ---- - -## 8. Error Codes - -```rust -pub enum CompressibleInstructionError { - InvalidRentSponsor, - MissingSeedAccount, - SeedMismatch, // data.* seeds don't match compressed account data - CTokenDecompressionNotImplemented, - PdaDecompressionNotImplemented, - TokenCompressionNotImplemented, - PdaCompressionNotImplemented, -} -``` - ---- - -## 9. Test Command - -```bash -cargo test-sbf -p csdk-anchor-full-derived-test -``` - ---- - -## 10. Architecture Diagram - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ CLIENT SIDE │ -├────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────┐ ┌──────────────────────────────────────┐ │ -│ │ RPC: get_compressed_ │ │ UserRecordSeeds │ │ -│ │ account() │ │ ┌────────────────────────────────┐ │ │ -│ │ ───────────────────► │ │ │ authority: Pubkey (ctx.*) │ │ │ -│ │ CompressedAccount { │ │ │ mint_authority: Pubkey(ctx.*) │ │ │ -│ │ data: bytes, │ │ │ owner: Pubkey (data.*) │ │ │ -│ │ hash, │ │ │ category_id: u64 (data.*) │ │ │ -│ │ tree_info, │ │ └────────────────────────────────┘ │ │ -│ │ } │ └──────────────────────────────────────┘ │ -│ └─────────────────────────┘ │ │ -│ │ │ │ -│ ▼ │ │ -│ ┌───────────────────────────────────────────────────────────────────────┐ │ -│ │ AccountInterface::cold(pda_address, compressed_account) │ │ -│ │ - pubkey: Pubkey (target PDA address) │ │ -│ │ - is_cold: true │ │ -│ │ - decompression_context: Some(compressed_account) │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -│ │ │ │ -│ └───────────────┬───────────────────┘ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────────────────────────────┐ │ -│ │ CompressedAccountVariant::user_record(interface.compressed_data(), seeds)│ -│ │ 1. Deserialize: UserRecord::deserialize(&data_bytes) │ │ -│ │ 2. Verify: data.owner == seeds.owner │ │ -│ │ 3. Verify: data.category_id == seeds.category_id │ │ -│ │ 4. Return: UserRecord { data, authority, mint_authority } │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────────────────────────────┐ │ -│ │ RentFreeDecompressAccount::new(account_interface, variant) │ │ -│ │ - account_interface: AccountInterface (pubkey + compressed data) │ │ -│ │ - variant: CompressedAccountVariant::UserRecord { ... } │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────────────────────────────┐ │ -│ │ decompress_accounts_idempotent_new( │ │ -│ │ program_id, │ │ -│ │ vec![decompress_account1, decompress_account2, ...], │ │ -│ │ &account_metas, │ │ -│ │ validity_proof, │ │ -│ │ None, // default discriminator │ │ -│ │ ) │ │ -│ │ │ │ -│ │ 1. Filter: keep only is_cold accounts │ │ -│ │ 2. Extract: pubkeys from account_interface │ │ -│ │ 3. Pack::pack() converts Pubkeys to indices │ │ -│ │ 4. Return: Some(Instruction) or None if all hot │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -└──────────────────────────────┼──────────────────────────────────────────────┘ - │ - ══════════════════════╪══════════════════════ TRANSACTION - │ - ▼ -┌────────────────────────────────────────────────────────────────────────────┐ -│ ON-CHAIN │ -├────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌───────────────────────────────────────────────────────────────────────┐ │ -│ │ 1. UNPACK (PackedUserRecord → UserRecord) │ │ -│ │ authority = post_system_accounts[authority_idx].key │ │ -│ │ mint_authority = post_system_accounts[mint_authority_idx].key │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────────────────────────────┐ │ -│ │ 2. DERIVE PDA │ │ -│ │ ctx_seeds = UserRecordCtxSeeds { authority, mint_authority } │ │ -│ │ seeds = ["user_record", │ │ -│ │ ctx_seeds.authority, │ │ -│ │ ctx_seeds.mint_authority, │ │ -│ │ self.owner, // from unpacked data │ │ -│ │ self.category_id] // from unpacked data │ │ -│ │ derived_pda = find_program_address(seeds, program_id) │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────────────────────────────┐ │ -│ │ 3. VERIFY & CREATE │ │ -│ │ assert!(derived_pda == target_account.key) │ │ -│ │ if !exists { create_pda() } │ │ -│ │ write_data(unpacked_data) │ │ -│ └───────────────────────────────────────────────────────────────────────┘ │ -│ │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## PHASE 8: CToken Seed Refactor (COMPLETED) - -### Current CToken Flow (Problem) - -``` -Client: - TokenAccountVariant::Vault // No fields - just an enum tag - + TokenData { owner, mint, amount } - → Pack: variant just CLONED (no packing) - -On-chain: - TokenSeedProvider::get_seeds(ctx.accounts, remaining_accounts) - → ctx.accounts.cmint.as_ref()?.key() // READS FROM NAMED ACCOUNT! - → derive PDA with ["vault", cmint] -``` - -**Problem**: CToken seed resolution still requires named accounts in `DecompressAccountsIdempotent`. - -### Target CToken Flow - -``` -Client: - TokenAccountVariant::Vault { cmint: Pubkey } // HAS SEED FIELD! - + TokenData { owner, mint, amount } - → Pack: variant.cmint → cmint_idx (pushed to remaining_accounts) - -On-chain: - Unpack: cmint_idx → post_system_accounts[cmint_idx].key → cmint Pubkey - TokenSeedProvider::get_seeds(program_id) - → self.cmint // READS FROM VARIANT DIRECTLY! - → derive PDA with ["vault", cmint] -``` - -**Result**: No named seed accounts needed. Same pattern as PDAs. - -### Generated Types (After Refactor) - -```rust -// Unpacked (client-side, with Pubkeys) -pub enum TokenAccountVariant { - Vault { cmint: Pubkey }, - UserAta { owner: Pubkey, cmint: Pubkey }, // If defined -} - -// Packed (wire format, with indices) -pub enum PackedTokenAccountVariant { - Vault { cmint_idx: u8 }, - UserAta { owner_idx: u8, cmint_idx: u8 }, -} - -// Pack impl -impl Pack for TokenAccountVariant { - type Packed = PackedTokenAccountVariant; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - match self { - TokenAccountVariant::Vault { cmint } => { - PackedTokenAccountVariant::Vault { - cmint_idx: remaining_accounts.insert_or_get(*cmint), - } - } - } - } -} - -// Unpack impl -impl Unpack for PackedTokenAccountVariant { - type Unpacked = TokenAccountVariant; - - fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result { - match self { - PackedTokenAccountVariant::Vault { cmint_idx } => { - Ok(TokenAccountVariant::Vault { - cmint: *remaining_accounts[*cmint_idx as usize].key, - }) - } - } - } -} -``` - -### TokenSeedProvider Trait Change - -```rust -// BEFORE (requires accounts struct) -pub trait TokenSeedProvider: Copy { - type Accounts<'info>; - - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, // Used for ctx.accounts.cmint - remaining_accounts: &'a [AccountInfo<'info>], - ) -> Result<(Vec>, Pubkey), ProgramError>; -} - -// AFTER (self-contained) -pub trait TokenSeedProvider: Copy { - fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; - fn get_authority_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; -} -``` - -### Generated TokenSeedProvider impl - -```rust -impl TokenSeedProvider for TokenAccountVariant { - fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError> { - match self { - TokenAccountVariant::Vault { cmint } => { - // cmint is already resolved Pubkey from variant! - let seeds: &[&[u8]] = &[b"vault", cmint.as_ref()]; - let (pda, bump) = Pubkey::find_program_address(seeds, program_id); - let mut seeds_vec = seeds.iter().map(|s| s.to_vec()).collect::>(); - seeds_vec.push(vec![bump]); - Ok((seeds_vec, pda)) - } - } - } -} -``` - -### DecompressAccountsIdempotent Simplification - -```rust -// BEFORE -pub struct DecompressAccountsIdempotent<'info> { - pub fee_payer: Signer<'info>, - pub config: AccountInfo<'info>, - pub rent_sponsor: UncheckedAccount<'info>, - // CToken static accounts - pub ctoken_rent_sponsor: Option>, - pub light_token_program: Option>, - pub ctoken_cpi_authority: Option>, - pub ctoken_config: Option>, - // SEED ACCOUNTS (needed by TokenSeedProvider) - pub authority: Option>, - pub mint_authority: Option>, - pub user: Option>, - pub cmint: Option>, - pub some_account: Option>, -} - -// AFTER -pub struct DecompressAccountsIdempotent<'info> { - pub fee_payer: Signer<'info>, - pub config: AccountInfo<'info>, - pub rent_sponsor: UncheckedAccount<'info>, - // CToken static accounts - pub ctoken_rent_sponsor: Option>, - pub light_token_program: Option>, - pub ctoken_cpi_authority: Option>, - pub ctoken_config: Option>, - // NO SEED ACCOUNTS - they're in the variant! -} -``` - -### Client Usage (After Refactor) - -```rust -// Construct CToken variant with seed pubkeys -let vault_variant = TokenAccountVariant::Vault { cmint: cmint_pda }; -let ctoken_data = CTokenData { - variant: vault_variant, - token_data: compressed_vault.token.clone(), -}; - -let decompress_instruction = compressible_instruction::decompress_accounts_idempotent_new( - &program_id, - vec![ - RentFreeDecompressAccount::new(user_interface, user_variant), - RentFreeDecompressAccount::new(vault_interface, CompressedAccountVariant::CTokenData { data: ctoken_data }), - ], - compressible_instruction::decompress::accounts(payer.pubkey(), config_pda, payer.pubkey()), - rpc_result, -)?; -``` - -### decompress::accounts Helper - -```rust -/// Returns program account metas for decompress_accounts_idempotent with CToken support. -/// Includes ctoken_rent_sponsor, light_token_program, ctoken_cpi_authority, ctoken_config. -pub fn accounts(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey) -> Vec; - -/// Returns program account metas for PDA-only decompression (no CToken accounts). -pub fn accounts_pda_only(fee_payer: Pubkey, config: Pubkey, rent_sponsor: Pubkey) -> Vec; -``` - -### SDK Changes Required - -| File | Changes | -| --------------------------------------------------- | -------------------------------------------------- | -| `ctoken-sdk/src/pack.rs` | Add Pack bound to V, use V::Packed for packed type | -| `sdk/src/compressible/decompress_runtime.rs` | Update TokenSeedProvider trait signature | -| `macros/src/compressible/variant_enum.rs` | Generate TokenAccountVariant with struct fields | -| `macros/src/compressible/seed_providers.rs` | Update get_seeds to use self.field | -| `macros/src/compressible/instructions.rs` | Remove seed account fields from Accounts struct | -| `ctoken-sdk/src/compressible/decompress_runtime.rs` | Update process_decompress_tokens_runtime | - -### Flow Diagram (After Phase 8) - -``` -┌────────────────────────────────────────────────────────────────────────────┐ -│ CLIENT SIDE │ -├────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ TokenAccountVariant::Vault { cmint: cmint_pda } │ -│ + TokenData { owner, mint, amount } │ -│ = CTokenData { variant, token_data } │ -│ │ │ -│ ▼ │ -│ Pack::pack() │ -│ variant.cmint → cmint_idx = remaining_accounts.insert_or_get(cmint) │ -│ token_data.owner → owner_idx │ -│ token_data.mint → mint_idx │ -│ = PackedCTokenData { variant: Vault { cmint_idx }, token_data } │ -│ │ │ -└──────────────────────────────┼──────────────────────────────────────────────┘ - │ - ══════════════════════╪══════════════════════ TRANSACTION - │ - ▼ -┌────────────────────────────────────────────────────────────────────────────┐ -│ ON-CHAIN │ -├────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ PackedCTokenData { variant: Vault { cmint_idx }, token_data } │ -│ │ │ -│ ▼ │ -│ Unpack::unpack(post_system_accounts) │ -│ cmint = post_system_accounts[cmint_idx].key │ -│ owner = post_system_accounts[owner_idx].key │ -│ = CTokenData { variant: Vault { cmint }, token_data } │ -│ │ │ -│ ▼ │ -│ TokenSeedProvider::get_seeds(program_id) │ -│ match self { │ -│ Vault { cmint } => seeds = ["vault", cmint.as_ref()] │ -│ } │ -│ = (seeds, derived_pda) │ -│ │ │ -│ ▼ │ -│ Verify: derived_pda == target_account.key │ -│ Create token account with seeds │ -│ │ -└────────────────────────────────────────────────────────────────────────────┘ -``` - -### Implementation Steps - -1. **Update SDK trait** (`sdk/src/compressible/decompress_runtime.rs`): - - Change `TokenSeedProvider` signature to not require `accounts` param - -2. **Update ctoken-sdk Pack** (`ctoken-sdk/src/pack.rs`): - - Add `Pack` trait bound to `V` in `CTokenDataWithVariant` - - Use `V::Packed` as the packed variant type - -3. **Generate CToken variant enums** (`variant_enum.rs`): - - Parse token_seeds to extract ctx.\* fields - - Generate `TokenAccountVariant` with struct variants (Pubkeys) - - Generate `PackedTokenAccountVariant` with struct variants (indices) - - Generate Pack/Unpack impls - -4. **Update seed provider generation** (`seed_providers.rs`): - - Change `get_seeds()` to use `self.cmint` instead of `ctx.accounts.cmint` - -5. **Remove seed accounts** (`instructions.rs`): - - Remove seed account fields from `DecompressAccountsIdempotent` - -6. **Update tests** (`basic_test.rs`): - - Construct `TokenAccountVariant::Vault { cmint }` with Pubkey - - Remove seed accounts from instruction building diff --git a/sdk-libs/macros/MACRO_REFACTOR.md b/sdk-libs/macros/MACRO_REFACTOR.md deleted file mode 100644 index 0627d2c898..0000000000 --- a/sdk-libs/macros/MACRO_REFACTOR.md +++ /dev/null @@ -1,518 +0,0 @@ -# Compressible Macro Refactor Plan - -## Goal - -Eliminate seed duplication by extracting seeds from Anchor's `#[account(seeds = [...])]` attributes instead of requiring separate declaration in `#[compressible(...)]`. - ---- - -## Current Architecture (Problems) - -### Dual Seed Declaration - -```rust -// Declaration 1: Anchor attribute (source of truth for on-chain PDA) -#[account( - seeds = [b"user_record", authority.key().as_ref(), params.owner.as_ref()], - bump, -)] -pub user_record: Account<'info, UserRecord>, - -// Declaration 2: Global compressible macro (DUPLICATED) -#[compressible( - UserRecord = (seeds = ("user_record", ctx.authority, data.owner)), - owner = Pubkey, -)] -``` - -**Problems:** - -- Seeds declared twice - can diverge -- Refactoring risk - change one, forget the other -- Runtime failures from seed mismatch -- Cognitive overhead - -### Current Generated Items - -From global `#[compressible(...)]`: - -- `CompressedAccountVariant` enum -- `PackedXxx` structs per type -- `SeedParams` struct for instruction data fields -- `DecompressAccountsIdempotent<'info>` with **named** seed accounts -- `CompressAccountsIdempotent<'info>` -- `PdaSeedDerivation` trait impls -- `TokenSeedProvider` trait impls -- Instruction handlers -- Client-side seed functions - ---- - -## Proposed Architecture - -### Single Source of Truth - -Seeds extracted from Anchor's `#[account(seeds = [...])]` attribute: - -```rust -#[derive(Accounts, LightCompressible)] -#[instruction(params: MyParams)] -pub struct CreateUserRecord<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - - pub authority: Signer<'info>, - - #[account( - init, - payer = fee_payer, - space = 8 + UserRecord::INIT_SPACE, - seeds = [b"user_record", authority.key().as_ref(), params.owner.as_ref()], - bump, - )] - #[compressible( - address_tree_info = params.address_tree_info, - output_tree = params.output_state_tree_index - )] - pub user_record: Account<'info, UserRecord>, - - pub system_program: Program<'info, System>, -} -``` - -**No duplicate seed declaration needed.** - -### Token Accounts - -For CToken accounts, reference the authority field directly: - -```rust -#[account(mut, seeds = [b"vault", cmint.key().as_ref()], bump)] -#[compressible_token( - address_tree_info = params.address_tree_info, - output_tree = params.output_state_tree_index, - authority = vault_authority, // Reference to authority field -)] -pub vault: UncheckedAccount<'info>, - -#[account(seeds = [b"vault_authority"], bump)] -pub vault_authority: UncheckedAccount<'info>, // Authority seeds extracted from here -``` - -### Token Authority Resolution - -The authority for a CToken account can be various types. The macro auto-detects or allows explicit override. - -#### Authority Types - -| Authority Type | Example | Seeds Needed? | -| ---------------- | ------------------------------------------------ | ----------------------------------- | -| PDA | `#[account(seeds = [b"vault_authority"], bump)]` | Yes - extract from field | -| Signer | `pub authority: Signer<'info>` | No - user signs directly | -| External/Dynamic | Stored in account data, passed differently | Explicit `authority_seeds` required | - -#### Auto-Detection Logic - -```rust -fn resolve_authority_seeds(token_field: &Field, accounts_struct: &ItemStruct) -> AuthoritySeeds { - let authority_field_name = get_authority_from_attr(token_field); - let authority_field = find_field(accounts_struct, authority_field_name); - - // Case 1: Field is Signer<'info> - user signs, no seeds needed - if is_signer_type(&authority_field.ty) { - return AuthoritySeeds::UserSigns; - } - - // Case 2: Field has #[account(seeds = [...])] - extract them - if let Some(seeds) = extract_anchor_seeds(&authority_field) { - return AuthoritySeeds::Pda(seeds); - } - - // Case 3: Check for explicit authority_seeds in attribute - if let Some(seeds) = get_explicit_authority_seeds(token_field) { - return AuthoritySeeds::Pda(seeds); - } - - // Case 4: Can't determine - compile error - compile_error!( - "Cannot determine authority seeds. Either:\n\ - - Add #[account(seeds = [...])] to the authority field, or\n\ - - Add authority_seeds = (...) to #[compressible_token], or\n\ - - Use Signer<'info> if user signs directly" - ) -} -``` - -#### Examples - -**PDA authority (auto-detected):** - -```rust -#[compressible_token(authority = vault_authority)] -pub vault: UncheckedAccount<'info>, - -#[account(seeds = [b"vault_authority"], bump)] // macro extracts these -pub vault_authority: UncheckedAccount<'info>, -``` - -**User signer (auto-detected):** - -```rust -#[compressible_token(authority = user)] -pub vault: UncheckedAccount<'info>, - -pub user: Signer<'info>, // macro sees Signer, no seeds needed -``` - -**Complex/dynamic seeds (explicit override):** - -```rust -#[compressible_token( - authority = pool_authority, - authority_seeds = (POOL_AUTH_SEED, pool_state.key()), // explicit -)] -pub vault: UncheckedAccount<'info>, - -/// CHECK: Authority derived from pool state -pub pool_authority: UncheckedAccount<'info>, // no #[account(seeds)] here -``` - -**External authority (must sign tx):** - -```rust -#[compressible_token( - authority = external_authority, - authority_is_signer, // authority must sign the tx directly -)] -pub vault: UncheckedAccount<'info>, - -/// CHECK: External multisig or other authority -pub external_authority: UncheckedAccount<'info>, -``` - -#### Attribute Spec - -```rust -#[compressible_token( - address_tree_info = , - output_tree = , - authority = , // Required: which field is the authority - - // Optional (mutually exclusive) - only needed if auto-detect fails: - authority_seeds = (, ...), // Explicit PDA seeds - authority_is_signer, // Authority signs tx directly (no PDA) -)] -``` - -### Complex Seed Expressions - -Authority seeds can contain arbitrary expressions: - -```rust -#[account( - seeds = [ - b"vault_authority", // Byte literal - VAULT_AUTH_SEED, // Constant - pool.key().as_ref(), // Account reference - params.pool_id.as_ref(), // Param reference - params.nonce.to_le_bytes().as_ref(), // Param with conversion - max_key(&a.key(), &b.key()).as_ref(), // Function call - ], - bump, -)] -pub vault_authority: UncheckedAccount<'info>, -``` - -#### Handling Strategy - -| Expression Type | Auto-detect? | Notes | -| ---------------------------- | ------------ | ------------------------------------- | -| `b"literal"` | Yes | Hardcoded | -| `CONSTANT` | Yes | Resolved at compile time | -| `account.key()` | Yes | Account must be in struct | -| `params.field` | Yes | If `#[instruction]` parsed | -| `params.field.to_le_bytes()` | Yes | Same, with method chain | -| `&account.data.field[..]` | Pass-through | Account must be deserialized | -| `function(args)` | Pass-through | Emitted as-is, compile error if wrong | - -#### Pass-Through Approach - -For expressions we can't fully classify, emit them as-is with rewrite rules: - -```rust -// Input: seeds = [VAULT_AUTH_SEED, max_key(&a.key(), &b.key()).as_ref()] - -// Generated code (rewritten): -fn derive_authority_seeds<'info>( - accounts: &MyAccounts<'info>, - _params: &MyParams, -) -> Result>, ProgramError> { - let seeds: Vec<&[u8]> = vec![ - VAULT_AUTH_SEED, // constant - direct - max_key(&accounts.a.key(), &accounts.b.key()).as_ref(), // rewritten: a -> accounts.a - ]; - // ... -} -``` - -**Rewrite rules:** - -- `field.key()` → `accounts.field.key()` -- `params.x` → `params.x` -- Everything else → pass through unchanged - -#### Failure Modes - -| Scenario | Result | Fix | -| --------------------- | ------------- | -------------------------------------- | -| Function not in scope | Compile error | Import the function | -| Account not in struct | Compile error | Add account to struct | -| Wrong type | Compile error | Fix types | -| Runtime logic differs | Runtime error | Use explicit `authority_seeds = (...)` | - -All failures are **explicit errors**, not silent bugs. - -### Module-Level Declaration (Minimal) - -Only need to declare which types form the enum: - -```rust -#[compressible_types(UserRecord, GameSession, PlaceholderRecord)] -#[program] -pub mod my_program { - // ... -} -``` - -Or potentially infer from `#[compressible]` fields across all Accounts structs. - ---- - -## Generated Items (Refactored) - -### Per-Account-Type Seed Struct - -From parsing `#[account(seeds = [b"user_record", authority.key().as_ref(), params.owner.as_ref()])]`: - -```rust -// Generated -pub struct UserRecordSeeds { - pub authority: Pubkey, // from `authority.key().as_ref()` - pub owner: Pubkey, // from `params.owner.as_ref()` -} - -impl UserRecordSeeds { - pub fn derive_pda(&self, program_id: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[b"user_record", self.authority.as_ref(), self.owner.as_ref()], - program_id, - ) - } -} -``` - -### Seed Classification - -The macro classifies each seed expression: - -| Expression Type | Classification | Generated Field | -| -------------------------- | ----------------- | -------------------------------------------- | -| `b"literal"` | Literal | (none - hardcoded) | -| `authority.key().as_ref()` | Account reference | `authority: Pubkey` | -| `params.owner.as_ref()` | Instruction data | `owner: Pubkey` (type from `#[instruction]`) | -| `CONSTANT` | Constant | (none - resolved at compile time) | - -### DecompressAccountsIdempotent (Refactored) - -**Option A: Remaining Accounts with Typed Indices** - -```rust -#[derive(Accounts)] -pub struct DecompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// CHECK: Validated by SDK - pub config: AccountInfo<'info>, - /// CHECK: Validated by SDK - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - // ... standard accounts only, NO named seed accounts -} - -// Client builds remaining_accounts with seed accounts -// SDK provides index mapping per variant -``` - -**Option B: Keep Named Optional Accounts (simpler)** - -Keep current approach but auto-generate from parsed seeds: - -```rust -#[derive(Accounts)] -pub struct DecompressAccountsIdempotent<'info> { - // ... standard accounts ... - - // Auto-generated from all unique account refs in seeds - /// CHECK: Optional seed account - #[account(mut)] - pub authority: Option>, - /// CHECK: Optional seed account - pub mint_authority: Option>, -} -``` - ---- - -## Trait Changes - -### PdaSeedDerivation - -```rust -// Current: Accounts struct + SeedParams -impl PdaSeedDerivation, SeedParams> for UserRecord { - fn derive_pda_seeds_with_accounts(&self, ..., accounts: &DecompressAccountsIdempotent, seed_params: &SeedParams) -> ... -} - -// Proposed: Just the typed seeds struct -impl PdaSeedDerivation for UserRecord { - type Seeds = UserRecordSeeds; - - fn derive_pda(seeds: &Self::Seeds, program_id: &Pubkey) -> (Pubkey, u8) { - seeds.derive_pda(program_id) - } -} -``` - ---- - -## Migration Path - -### Phase 1: Add New Derive Macro - -- Implement `LightCompressible` derive macro -- Parses `#[account(seeds = [...])]` from Anchor attribute -- Generates `XxxSeeds` structs -- Coexists with current `#[compressible(...)]` - -### Phase 2: Update DecompressAccountsIdempotent Generation - -- Auto-generate from parsed seeds across all Accounts structs -- Or use remaining_accounts approach -- Update `process_decompress_accounts_idempotent` to use new traits - -### Phase 3: Simplify Module-Level Macro - -- Reduce to just type list: `#[compressible_types(UserRecord, GameSession)]` -- Or remove entirely if types can be inferred - -### Phase 4: Deprecate Old Syntax - -- Emit warnings for old `#[compressible(Type = (seeds = ...))]` syntax -- Eventually remove - ---- - -## Implementation Details - -### Parsing Anchor Seeds Attribute - -```rust -fn extract_anchor_seeds(field: &syn::Field) -> Option> { - for attr in &field.attrs { - if attr.path().is_ident("account") { - // Parse: #[account(seeds = [...], bump, ...)] - // Extract the seeds = [...] part - // Return parsed seed expressions - } - } - None -} - -enum SeedExpr { - Literal(Vec), // b"user_record" - AccountRef(Ident), // authority.key().as_ref() -> authority - ParamRef(Ident, Type), // params.owner.as_ref() -> (owner, Pubkey) - Constant(Path), // MY_SEED -} -``` - -### Extracting Type from #[instruction] - -```rust -#[derive(Accounts, LightCompressible)] -#[instruction(params: MyParams)] -pub struct CreateUserRecord<'info> { ... } - -// Macro reads #[instruction(params: MyParams)] -// Then resolves MyParams to get field types for params.xxx references -``` - -This requires either: - -1. The params type to be in the same module (can resolve) -2. User annotation: `#[compressible(params_type = MyParams)]` -3. Accept just the field name, infer type as Pubkey/u64/etc. - ---- - -## Client-Side Changes - -### Current - -```rust -let decompress_account = CompressedAccountData { - data: CompressedAccountVariant::UserRecord(packed_data), - meta: compressed_account_meta, -}; - -// + SeedParams struct -// + named accounts in instruction -``` - -### Proposed - -```rust -let decompress_input = DecompressInput { - variant: CompressedAccountVariant::UserRecord(packed_data), - seeds: UserRecordSeeds { authority, owner }, // Typed! - compressed_account: compressed_account_data, -}; - -// Seeds struct is type-safe, IDE autocomplete works -``` - ---- - -## Open Questions - -1. **Remaining accounts vs named optional accounts for decompress?** - - Named: simpler, current approach, more readable - - Remaining: more flexible, less struct bloat - -2. ~~**How to handle token authority seeds?**~~ **RESOLVED** - - Auto-detect from authority field's `#[account(seeds)]` or `Signer` type - - Explicit `authority_seeds = (...)` as fallback - - `authority_is_signer` for external signers - - See "Token Authority Resolution" section - -3. **Type inference for params.xxx references?** - - Parse `#[instruction]` attribute for type - - Or require explicit annotation - - Or default to common types (Pubkey, u64) - -4. **Enum generation without module-level macro?** - - Scan all files for `#[compressible]` fields? - - Explicit type list still needed? - ---- - -## Benefits Summary - -| Aspect | Current | Proposed | -| ---------------------- | ------------------------- | -------------------- | -| Seed declarations | 2 (Anchor + compressible) | 1 (Anchor only) | -| Sync bugs possible | Yes | No | -| Refactoring safety | Low | High | -| Type-safe seed structs | Partial | Full | -| IDE support | Limited | Better (typed seeds) | -| Maintenance burden | High | Low | diff --git a/sdk-libs/macros/MACRO_REFACTOR_V2.md b/sdk-libs/macros/MACRO_REFACTOR_V2.md deleted file mode 100644 index 8beec9be22..0000000000 --- a/sdk-libs/macros/MACRO_REFACTOR_V2.md +++ /dev/null @@ -1,642 +0,0 @@ -# Compressible Macro Refactor V2 - Single Source of Truth - -## Status: ✅ FULLY IMPLEMENTED - -### What Works: - -- `#[compressible_program]` macro - **works across separate files!** -- PDA seed extraction from Anchor `#[account(seeds = [...])]` works -- Token field `#[compressible_token(Variant, authority = [...])]` extraction and codegen works -- Supports: byte literals, string literals, constants, `ctx.field.key()`, `params.field`, function calls -- File scanner recursively reads all `.rs` files in `src/` directory at compile time - -### How It Works: - -The `#[compressible_program]` macro bypasses proc macro limitations by directly reading and parsing -source files from the crate's `src/` directory. This "hidden export/import" pattern allows it to: - -1. Find all `#[derive(Accounts)]` structs across any file -2. Extract `#[compressible]` and `#[compressible_token]` marked fields -3. Parse their `#[account(seeds = [...])]` attributes -4. Generate all required code in the program module - -### Usage: - -```rust -// lib.rs - just add the macro, seeds come from Accounts structs automatically! -#[compressible_program] -#[program] -pub mod my_program { ... } - -// instruction_accounts.rs (separate file - works!) -#[derive(Accounts, LightFinalize)] -pub struct CreateAccounts<'info> { - #[account(seeds = [b"user", authority.key().as_ref()], bump)] - #[compressible] - pub user_record: Account<'info, UserRecord>, - - #[account(seeds = [b"vault", cmint.key().as_ref()], bump)] - #[compressible_token(Vault, authority = [b"vault_authority"])] - pub vault: UncheckedAccount<'info>, -} -``` - ---- - -## Executive Summary - -Replace the dual-declaration system with a single source of truth: - -- **BEFORE**: Seeds declared twice (global `#[compressible(...)]` + Anchor `#[account(seeds)]`) -- **AFTER**: Seeds extracted from Anchor's `#[account(seeds)]` attribute automatically - ---- - -## System Architecture - -``` -┌─────────────────────────────────────────────────────────────────────────────────────────┐ -│ COMPILE TIME │ -├─────────────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────────────────┐ ┌─────────────────────────────────────────────┐ │ -│ │ Module Level Macro │ │ Accounts Struct (can be in separate file!) │ │ -│ │ │ │ │ │ -│ │ #[compressible_program]│ │ #[derive(Accounts, LightFinalize)] │ │ -│ │ #[program] │ │ #[instruction(params: MyParams)] │ │ -│ │ pub mod my_program {} │ │ pub struct CreateAccounts<'info> { │ │ -│ │ │ │ │ │ -│ │ (File scanner reads │ │ #[account( │ │ -│ │ all .rs files in │ │ init, payer = fee_payer, │ │ -│ │ src/ directory) │ │ seeds = [b"user", auth.key().as_ref(), │ │ -│ │ │ │ params.owner.as_ref()], │ │ -│ └────────────┬────────────┘ │ bump, │ │ -│ │ │ )] │ │ -│ │ Scans for │ #[compressible] │ │ -│ │ #[compressible] │ pub user: Account<'info, UserRecord>, │ │ -│ │ fields │ │ │ -│ │ │ #[account(seeds = [b"vault", cmint...], │ │ -│ │ │ bump)] │ │ -│ │ │ #[compressible_token(Vault, authority=.)]│ │ -│ │ │ pub vault: UncheckedAccount<'info>, │ │ -│ │ │ } │ │ -│ │ └───────────────────┬─────────────────────────┘ │ -│ │ │ │ -│ │ │ Provides seeds, types │ -│ │ │ │ -│ └──────────────┬───────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌───────────────────────────────────────────────────────────────────────────────────┐ │ -│ │ CODEGEN OUTPUT │ │ -│ │ │ │ -│ │ 1. CompressedAccountVariant enum (with struct variants) │ │ -│ │ 2. PackedCompressedAccountVariant (with idx fields) │ │ -│ │ 3. TokenAccountVariant enum │ │ -│ │ 4. Pack/Unpack impls │ │ -│ │ 5. XxxSeeds structs per PDA type │ │ -│ │ 6. PdaSeedDerivation trait impls │ │ -│ │ 7. TokenSeedProvider trait impls │ │ -│ │ 8. DecompressAccountsIdempotent Accounts struct │ │ -│ │ 9. decompress_accounts_idempotent() instruction handler │ │ -│ │ 10. Client-side seed derivation functions │ │ -│ │ │ │ -│ └───────────────────────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Seed Extraction Flow - -``` -┌──────────────────────────────────────────────────────────────────────────────────────────┐ -│ ANCHOR ATTRIBUTE PARSING │ -├──────────────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Input: #[account(seeds = [b"user", auth.key().as_ref(), params.owner.as_ref()], bump)] │ -│ │ │ -│ ▼ │ -│ ┌──────────────────────────────────────────────────────────────────────────────────┐ │ -│ │ SEED EXPRESSION PARSER │ │ -│ │ │ │ -│ │ For each element in seeds array: │ │ -│ │ │ │ -│ │ ┌─────────────────┬────────────────────────────────────────────────────────┐ │ │ -│ │ │ Expression Type │ Classification & Generated Code │ │ │ -│ │ ├─────────────────┼────────────────────────────────────────────────────────┤ │ │ -│ │ │ b"literal" │ Literal → Hardcoded: &[0x75, 0x73, 0x65, 0x72] │ │ │ -│ │ │ "string" │ Literal → Hardcoded: "string".as_bytes() │ │ │ -│ │ │ CONSTANT │ Constant → crate::CONSTANT.as_ref() │ │ │ -│ │ │ auth.key() │ CtxAccount → ctx_seeds.auth field (Pubkey) │ │ │ -│ │ │ params.owner │ DataField → self.owner from deserialized data │ │ │ -│ │ │ params.id.to_le_bytes() │ DataField → self.id.to_le_bytes() │ │ │ -│ │ │ max_key(&a,&b) │ FnCall → pass through with field mapping │ │ │ -│ │ └─────────────────┴────────────────────────────────────────────────────────┘ │ │ -│ └──────────────────────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Output: SeedSpec { literals, ctx_fields: [auth], data_fields: [owner] } │ -│ │ -└──────────────────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Account Type Detection (Robustness) - -``` -┌───────────────────────────────────────────────────────────────────────────────────────────┐ -│ SUPPORTED ANCHOR ACCOUNT TYPES │ -├───────────────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌────────────────────────────────────────┬───────────────────────────────────────────┐ │ -│ │ Type Pattern │ Extraction Strategy │ │ -│ ├────────────────────────────────────────┼───────────────────────────────────────────┤ │ -│ │ Account<'info, T> │ Direct: inner_type = T │ │ -│ │ Box> │ Unwrap Box: inner_type = T │ │ -│ │ AccountLoader<'info, T> │ Direct: inner_type = T (zero-copy) │ │ -│ │ InterfaceAccount<'info, T> │ Direct: inner_type = T (SPL interface) │ │ -│ │ Box> │ Unwrap Box: inner_type = T │ │ -│ │ UncheckedAccount<'info> │ No type - for tokens only (explicit map) │ │ -│ │ AccountInfo<'info> │ No type - for tokens only (explicit map) │ │ -│ └────────────────────────────────────────┴───────────────────────────────────────────┘ │ -│ │ -│ DETECTION ALGORITHM: │ -│ │ -│ fn extract_account_inner_type(ty: &Type) -> Option<(bool, Ident)> { │ -│ match ty { │ -│ // Direct Account │ -│ Type::Path { segments: [.., Segment { ident: "Account", args: <'_, T> }] } │ -│ => Some((false, T)) │ -│ │ -│ // Box> │ -│ Type::Path { segments: [.., Segment { ident: "Box", args: > }] } │ -│ => Some((true, T)) │ -│ │ -│ // AccountLoader │ -│ Type::Path { segments: [.., Segment { ident: "AccountLoader", args: <'_, T> }] } │ -│ => Some((false, T)) │ -│ │ -│ // InterfaceAccount │ -│ Type::Path { segments: [.., Segment { ident: "InterfaceAccount", args }] } │ -│ => Some((false, T)) │ -│ │ -│ // Box> │ -│ Type::Path { segments: [.., "Box", args: > }] } │ -│ => Some((true, T)) │ -│ │ -│ _ => None // UncheckedAccount, AccountInfo - no inner type │ -│ } │ -│ } │ -│ │ -└───────────────────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## Data Flow: Creation to Decompression - -``` -┌─────────────────────────────────────────────────────────────────────────────────────────┐ -│ CREATION FLOW │ -├─────────────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ CLIENT │ -│ ─────── │ -│ 1. Derive PDA address: Pubkey::find_program_address(seeds, program_id) │ -│ 2. Call get_create_accounts_proof(pda_addresses) │ -│ 3. Build instruction with params containing create_accounts_proof │ -│ │ -│ │ │ -│ ▼ │ -│ ON-CHAIN (pre_init) │ -│ ──────────────────── │ -│ 1. LightFinalize parses #[compressible] fields │ -│ 2. For each field: │ -│ - Extract seeds from Anchor #[account(seeds = [...])] │ -│ - Derive compressed address: derive_address(pda_key, tree, program_id) │ -│ - Write to CPI context OR invoke Light System Program │ -│ 3. PDA initialized on-chain + compressed address registered │ -│ │ -│ │ │ -│ ▼ │ -│ RESULT │ -│ ────── │ -│ - On-chain PDA at: Pubkey::find_program_address(seeds, program_id) │ -│ - Compressed address at: derive_address(pda_key, tree, program_id) │ -│ - Data written to PDA │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────────────┘ - -┌─────────────────────────────────────────────────────────────────────────────────────────┐ -│ DECOMPRESSION FLOW │ -├─────────────────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ CLIENT │ -│ ─────── │ -│ 1. Fetch compressed account from indexer (by compressed address) │ -│ 2. Create AccountInterface::cold(pda_address, compressed_account) │ -│ 3. Create variant with seeds: │ -│ CompressedAccountVariant::user_record( │ -│ interface.compressed_data(), │ -│ UserRecordSeeds { auth, owner } // ctx.* + data.* seeds │ -│ ) │ -│ 4. Pack variant → indices into remaining_accounts │ -│ 5. Build & send decompress_accounts_idempotent instruction │ -│ │ -│ │ │ -│ ▼ │ -│ ON-CHAIN (decompress_accounts_idempotent) │ -│ ────────────────────────────────────────── │ -│ 1. Unpack: idx fields → Pubkey from remaining_accounts │ -│ 2. Deserialize compressed account data │ -│ 3. Build seeds: [literal, ctx_seeds.auth, self.owner, ...] │ -│ 4. Derive PDA: Pubkey::find_program_address(seeds, program_id) │ -│ 5. Verify: derived_pda == target_account.key │ -│ 6. Create PDA if not exists, write data │ -│ │ -│ │ │ -│ ▼ │ -│ RESULT │ -│ ────── │ -│ - On-chain PDA recreated with original data │ -│ - Compressed account consumed (nullified) │ -│ │ -└─────────────────────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## New Syntax Specification - -### Module Level (Simplified) - -```rust -// BEFORE (old - verbose, error-prone, seeds declared twice) -#[compressible( - UserRecord = (seeds = ("user_record", ctx.authority, data.owner, data.category_id.to_le_bytes())), - GameSession = (seeds = (GAME_SESSION_SEED, max_key(&ctx.user.key(), &ctx.authority.key()), data.session_id.to_le_bytes())), - Vault = (is_token, seeds = ("vault", ctx.cmint), authority = ("vault_authority")), - owner = Pubkey, - category_id = u64, - session_id = u64, -)] -#[program] -pub mod my_program { ... } - -// AFTER (new - no type list needed! Seeds extracted from Accounts structs) -#[compressible_program] // Just this! Scans src/ for #[compressible] fields -#[program] -pub mod my_program { ... } -``` - -### Accounts Struct (Seeds from Anchor) - -```rust -#[derive(Accounts, LightFinalize)] -#[instruction(params: CreateParams)] -pub struct CreateAccounts<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - - pub authority: Signer<'info>, - - // PDA - type extracted, seeds from #[account(...)] - #[account( - init, - payer = fee_payer, - space = 8 + UserRecord::INIT_SPACE, - seeds = [b"user_record", authority.key().as_ref(), params.owner.as_ref()], - bump, - )] - #[compressible] // Marker only - no seed duplication - pub user_record: Account<'info, UserRecord>, - - // Also works with Box - #[account( - init, - payer = fee_payer, - space = 8 + GameSession::INIT_SPACE, - seeds = [GAME_SESSION_SEED.as_bytes(), max_key(&fee_payer.key(), &authority.key()).as_ref()], - bump, - )] - #[compressible] - pub game_session: Box>, - - // Token - explicit variant + authority seeds (required for UncheckedAccount) - #[account( - mut, - seeds = [b"vault", cmint.key().as_ref()], - bump, - )] - #[compressible_token(Vault, authority = [b"vault_authority"])] // Variant + authority seeds - pub vault: UncheckedAccount<'info>, - - pub compression_config: AccountInfo<'info>, - pub system_program: Program<'info, System>, -} -``` - ---- - -## Macro Implementation (DONE ✅) - -### Files Modified - -| File | Purpose | Status | -| ------------------------------------------- | ---------------------------- | ------------------------------------------ | -| `macros/src/lib.rs` | Entry points | ✅ Added `compressible_program` proc macro | -| `macros/src/compressible/instructions.rs` | Generate code from seeds | ✅ Refactored for new seed source | -| `macros/src/compressible/file_scanner.rs` | **NEW** Scan src/ for fields | ✅ Implemented - reads external .rs files | -| `macros/src/compressible/anchor_seeds.rs` | Extract seeds from Anchor | ✅ Full seed classification | -| `macros/src/compressible/variant_enum.rs` | Generate enum | ✅ Uses extracted seed info | -| `macros/src/compressible/seed_providers.rs` | CToken seed provider | ✅ Adapted for new format | - -### New Parsing Logic - -```rust -// In finalize/parse.rs - -/// Parsed seed element from Anchor #[account(seeds = [...])] -#[derive(Clone, Debug)] -pub enum ParsedSeedElement { - /// b"literal" or "string" - Literal(Vec), - /// Compile-time constant: SOME_SEED - Constant(syn::Path), - /// Account reference: authority.key().as_ref() - CtxAccount(syn::Ident), - /// Param/data reference: params.owner.as_ref() - DataField { - field_name: syn::Ident, - method_chain: Option, // e.g., to_le_bytes - }, - /// Function call: max_key(&a.key(), &b.key()) - FunctionCall { - func: syn::Path, - ctx_args: Vec, // Account references in args - }, -} - -/// Extract seeds from Anchor #[account(seeds = [...], bump)] attribute -fn extract_anchor_seeds(field: &syn::Field) -> Option> { - for attr in &field.attrs { - if !attr.path().is_ident("account") { - continue; - } - - // Parse the attribute content - let meta_list = attr.parse_args_with( - Punctuated::::parse_terminated - ).ok()?; - - for meta in meta_list { - if let Meta::NameValue(nv) = meta { - if nv.path.is_ident("seeds") { - // Parse seeds = [...] - return parse_seeds_array(&nv.value); - } - } - } - } - None -} - -/// Classify a seed expression -fn classify_seed_expr(expr: &syn::Expr) -> ParsedSeedElement { - match expr { - // b"literal" - Expr::Lit(ExprLit { lit: Lit::ByteStr(bs), .. }) => { - ParsedSeedElement::Literal(bs.value()) - } - - // "string".as_bytes() or just "string" - Expr::Lit(ExprLit { lit: Lit::Str(s), .. }) => { - ParsedSeedElement::Literal(s.value().into_bytes()) - } - - // CONSTANT (all uppercase) - Expr::Path(path) if is_constant_path(&path.path) => { - ParsedSeedElement::Constant(path.path.clone()) - } - - // authority.key().as_ref() -> CtxAccount - Expr::MethodCall(mc) if is_ctx_account_ref(mc) => { - let field_name = extract_receiver_ident(mc); - ParsedSeedElement::CtxAccount(field_name) - } - - // params.owner.as_ref() -> DataField - Expr::MethodCall(mc) if is_params_field_ref(mc) => { - let (field_name, method) = extract_params_field(mc); - ParsedSeedElement::DataField { field_name, method_chain: method } - } - - // max_key(&a.key(), &b.key()).as_ref() -> FunctionCall - Expr::MethodCall(mc) if is_function_call_ref(mc) => { - let (func, ctx_args) = extract_function_call(mc); - ParsedSeedElement::FunctionCall { func, ctx_args } - } - - _ => panic!("Unsupported seed expression: {:?}", expr), - } -} -``` - ---- - -## Generated Code Examples - -### Seeds Struct - -```rust -// Generated from extracted seeds -pub struct UserRecordSeeds { - // From ctx.* seed elements - pub authority: Pubkey, - // From data.* seed elements (for verification) - pub owner: Pubkey, - pub category_id: u64, -} - -impl UserRecordSeeds { - pub fn derive_pda(&self, program_id: &Pubkey) -> (Pubkey, u8) { - let seeds: &[&[u8]] = &[ - b"user_record", - self.authority.as_ref(), - self.owner.as_ref(), - &self.category_id.to_le_bytes(), - ]; - Pubkey::find_program_address(seeds, program_id) - } -} -``` - -### Variant Constructor - -```rust -impl CompressedAccountVariant { - pub fn user_record( - account_data: &[u8], - seeds: UserRecordSeeds, - ) -> Result { - let data = UserRecord::deserialize(&mut &account_data[..])?; - - // Verify data.* seeds match compressed account - if data.owner != seeds.owner { - return Err(CompressibleInstructionError::SeedMismatch.into()); - } - if data.category_id != seeds.category_id { - return Err(CompressibleInstructionError::SeedMismatch.into()); - } - - Ok(Self::UserRecord { - data, - authority: seeds.authority, - }) - } -} -``` - ---- - -## Footguns & Robustness - -### 1. Type Listed but No Matching Account Field - -**Scenario**: `#[compressible_types(UserRecord)]` but no `Account` field - -**Solution**: Compile-time error with clear message - -``` -error: Type 'UserRecord' listed in #[compressible_types] but no matching - Account or Box> field found in any - #[derive(Accounts)] struct with #[compressible] attribute. - --> lib.rs:50:1 -``` - -### 2. Multiple Instructions with Different Seeds - -**Scenario**: Same type used with different seeds in different instructions - -**Solution**: Currently unsupported - emit error - -``` -error: Type 'UserRecord' has conflicting seed definitions: - - In CreateUserRecord: seeds = [b"user_v1", ...] - - In MigrateUserRecord: seeds = [b"user_v2", ...] - -Consider using different types for different PDA schemes. -``` - -### 3. Seed Expression Not Recognized - -**Scenario**: Complex expression macro can't parse - -**Solution**: Emit helpful error with workaround - -``` -error: Unable to parse seed expression. Supported patterns: - - Literals: b"seed", "seed" - - Constants: MY_SEED (uppercase) - - Account refs: account.key().as_ref() - - Params: params.field.as_ref(), params.field.to_le_bytes().as_ref() - - Functions: my_fn(&a.key(), &b.key()).as_ref() - -If your expression doesn't match, use explicit #[compressible(seeds = (...))] -override on the field. -``` - -### 4. params.\* Type Inference - -**Scenario**: Need to know type of `params.owner` for seeds struct - -**Solution**: Infer from data struct fields by name matching - -```rust -// Seeds: params.owner.as_ref() -// UserRecord has: owner: Pubkey -// Therefore: UserRecordSeeds.owner: Pubkey - -// Seeds: params.category_id.to_le_bytes().as_ref() -// UserRecord has: category_id: u64 -// Therefore: UserRecordSeeds.category_id: u64 -``` - -If no match found, default to `Pubkey` for `.as_ref()`, `u64` for `.to_le_bytes()`. - -### 5. Token Authority Resolution - -**Scenario**: Need authority seeds for CToken accounts - -**Solution**: Authority seeds are specified inline in the `#[compressible_token]` attribute: - -```rust -// Authority seeds are required and specified inline -#[account(mut, seeds = [b"vault", cmint.key().as_ref()], bump)] -#[compressible_token(Vault, authority = [b"vault_authority"])] -pub vault: UncheckedAccount<'info>, -``` - -The `authority = [...]` parameter specifies the seeds used to derive the CToken authority PDA -for signing during compression operations. - ---- - -## Migration Guide - -### Step 1: Update Module Level - -```rust -// REMOVE this: -#[compressible( - UserRecord = (seeds = ("user_record", ctx.authority, data.owner)), - owner = Pubkey, -)] - -// ADD this: -#[compressible_program] // No type list needed! -``` - -### Step 2: Ensure Anchor Seeds Are Defined - -Your `#[account(seeds = [...])]` attributes already contain the seeds - no changes needed there! - -### Step 3: Add #[compressible] to PDA Fields - -```rust -#[account(init, seeds = [...], bump)] -#[compressible] // Add this marker -pub user_record: Account<'info, UserRecord>, -``` - -### Step 4: For Tokens, Add Explicit Mapping with Authority - -```rust -#[account(mut, seeds = [...], bump)] -#[compressible_token(Vault, authority = [b"vault_authority"])] // Variant + authority seeds -pub vault: UncheckedAccount<'info>, -``` - ---- - -## Test Plan ✅ PASSED - -1. **Unit Tests**: Parse Anchor seeds correctly for all supported types ✅ -2. **Integration Tests**: Full create → compress → decompress cycle ✅ -3. **Edge Cases**: Box, AccountLoader, function call seeds ✅ -4. **Error Cases**: Missing types, conflicting seeds, unparseable expressions ✅ -5. **Migration**: Verified with csdk-anchor-full-derived-test ✅ - ---- - -## Implementation Order (ALL COMPLETE ✅) - -1. **Phase 1**: Add Anchor seed extraction to `anchor_seeds.rs` ✅ -2. **Phase 2**: Create file_scanner.rs to read external .rs files ✅ -3. **Phase 3**: Wire extracted seeds to codegen in `instructions.rs` ✅ -4. **Phase 4**: Add `#[compressible_program]` module-level macro ✅ -5. **Phase 5**: Generate all required code (enums, traits, decompress, compress) ✅ -6. **Phase 6**: Update csdk-anchor-full-derived-test to use new syntax ✅ -7. **Phase 7**: All tests passing! ✅ diff --git a/sdk-libs/macros/OPTION_A_PLAN.md b/sdk-libs/macros/OPTION_A_PLAN.md deleted file mode 100644 index 7504c14e7d..0000000000 --- a/sdk-libs/macros/OPTION_A_PLAN.md +++ /dev/null @@ -1,406 +0,0 @@ -# Option A Implementation Plan - -## Executive Summary - -Add `StandardAta` and `PackedStandardAta` as always-present variants in `CompressedAccountVariant` to enable decompression of arbitrary ATAs without program-specific enum variants. - -**Key insight**: The existing `CompressedMint` variant already handles standard mints. We only need to add support for standard ATAs. - ---- - -## Phase 1: Data Structures (ctoken-sdk/src/pack.rs) - -### 1.1 Add StandardAtaData struct - -```rust -/// Standard ATA data for decompression. -/// The wallet owner signs the transaction (not the program). -/// TokenData.owner = ATA address (derived from wallet + mint). -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct StandardAtaData { - /// Wallet owner pubkey - MUST be a signer on the transaction. - pub wallet: Pubkey, - /// Mint pubkey for this token account. - pub mint: Pubkey, - /// Token data from compressed account. - /// CRITICAL: token_data.owner = ATA address (not wallet). - pub token_data: TokenData, -} -``` - -### 1.2 Add PackedStandardAtaData struct - -```rust -/// Packed StandardAtaData with indices into remaining_accounts. -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct PackedStandardAtaData { - /// Index of wallet in remaining_accounts (must be signer). - pub wallet_index: u8, - /// Index of mint in remaining_accounts. - pub mint_index: u8, - /// Index of ATA address in remaining_accounts. - pub ata_index: u8, - /// Packed token data (owner/delegate/mint are indices). - pub token_data: InputTokenDataCompressible, -} -``` - -### 1.3 Implement Pack/Unpack traits - -```rust -impl Pack for StandardAtaData { - type Packed = PackedStandardAtaData; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - let (ata_address, _bump) = - crate::token::get_associated_ctoken_address_and_bump(&self.wallet, &self.mint); - - // Insert wallet as signer - let wallet_index = remaining_accounts.insert_or_get_config(self.wallet, true, false); - let mint_index = remaining_accounts.insert_or_get(self.mint); - let ata_index = remaining_accounts.insert_or_get(ata_address); - - PackedStandardAtaData { - wallet_index, - mint_index, - ata_index, - token_data: self.token_data.pack(remaining_accounts), - } - } -} - -impl Unpack for PackedStandardAtaData { - type Unpacked = StandardAtaData; - - fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result { - let wallet = *remaining_accounts - .get(self.wallet_index as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)? - .key; - let mint = *remaining_accounts - .get(self.mint_index as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)? - .key; - let token_data = self.token_data.unpack(remaining_accounts)?; - - Ok(StandardAtaData { wallet, mint, token_data }) - } -} -``` - ---- - -## Phase 2: Macro Changes (sdk-libs/macros) - -### 2.1 variant_enum.rs - Add StandardAta variants - -Update `compressed_account_variant` to include StandardAta variants: - -```rust -let enum_def = quote! { - #[derive(Clone, Debug, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] - pub enum CompressedAccountVariant { - #(#account_variants)* - PackedCTokenData(light_token_sdk::compat::PackedCTokenData), - CTokenData(light_token_sdk::compat::CTokenData), - CompressedMint(light_token_sdk::compat::CompressedMintData), - // NEW: Standard ATA variants - StandardAta(light_token_sdk::compat::StandardAtaData), - PackedStandardAta(light_token_sdk::compat::PackedStandardAtaData), - } -}; -``` - -### 2.2 variant_enum.rs - Update trait implementations - -Add match arms for StandardAta in: - -- `DataHasher` impl (unreachable for packed) -- `HasCompressionInfo` impl (unreachable - token accounts don't have compression_info) -- `Size` impl (unreachable) -- `Pack` impl (StandardAta -> PackedStandardAta) -- `Unpack` impl (PackedStandardAta -> StandardAta) - -### 2.3 decompress_context.rs - Update collect_all_accounts - -Add handling for StandardAta in `collect_all_accounts`: - -```rust -CompressedAccountVariant::PackedStandardAta(data) => { - // Standard ATAs are processed alongside program tokens - // They share the same token processing path with is_ata=true behavior - standard_ata_accounts.push((data, meta)); -} -CompressedAccountVariant::StandardAta(_) => { - unreachable!("Unpacked StandardAta should not appear in packed instruction data"); -} -``` - -### 2.4 instructions.rs - Update collect_all_accounts helper - -Modify `collect_all_accounts` to return standard ATAs as a fourth tuple element: - -```rust -fn collect_all_accounts<'a, 'b, 'info>( - // ... params ... -) -> Result<( - Vec, - Vec<(PackedCTokenData, Meta)>, - Vec<(CompressedMintData, Meta)>, - Vec<(PackedStandardAtaData, Meta)>, // NEW -), ProgramError> -``` - ---- - -## Phase 3: Runtime Changes (ctoken-sdk/src/compressible) - -### 3.1 decompress_runtime.rs - Process standard ATAs - -Add processing for standard ATAs in `process_decompress_tokens_runtime`: - -```rust -/// Process standard ATAs alongside program tokens. -/// Standard ATAs use the same Transfer2 CPI but: -/// 1. Don't require program-derived seeds -/// 2. Wallet must be a TX signer (validated here) -/// 3. ATA is derived from (wallet, light_token_program, mint) -pub fn process_standard_atas_in_token_flow<'info>( - standard_atas: Vec<(PackedStandardAtaData, CompressedAccountMetaNoLamportsNoAddress)>, - packed_accounts: &[AccountInfo<'info>], - fee_payer: &AccountInfo<'info>, - ctoken_config: &AccountInfo<'info>, - ctoken_rent_sponsor: &AccountInfo<'info>, - cpi_accounts: &CpiAccounts<'_, 'info>, - token_decompress_indices: &mut Vec, -) -> Result<(), ProgramError> { - for (packed_ata, meta) in standard_atas { - let wallet_info = &packed_accounts[packed_ata.wallet_index as usize]; - let mint_info = &packed_accounts[packed_ata.mint_index as usize]; - let ata_info = &packed_accounts[packed_ata.ata_index as usize]; - - // 1. Verify wallet is signer - if !wallet_info.is_signer { - msg!("StandardAta wallet must be signer: {:?}", wallet_info.key); - return Err(ProgramError::MissingRequiredSignature); - } - - // 2. Verify ATA derivation - let (derived_ata, bump) = derive_ctoken_ata(wallet_info.key, mint_info.key); - if derived_ata != *ata_info.key { - msg!("ATA derivation mismatch"); - return Err(ProgramError::InvalidAccountData); - } - - // 3. Create ATA (idempotent) - CreateAssociatedCTokenAccountCpi { - payer: fee_payer.clone(), - associated_token_account: ata_info.clone(), - owner: wallet_info.clone(), - mint: mint_info.clone(), - system_program: cpi_accounts.system_program()?.clone(), - bump, - compressible: CompressibleParamsCpi { - compressible_config: ctoken_config.clone(), - rent_sponsor: ctoken_rent_sponsor.clone(), - system_program: cpi_accounts.system_program()?.clone(), - pre_pay_num_epochs: 2, - lamports_per_write: None, - compress_to_account_pubkey: None, - token_account_version: TokenDataVersion::ShaFlat, - compression_only: true, - }, - idempotent: true, - }.invoke()?; - - // 4. Build decompress indices with TLV for ATA - let wallet_account_index = packed_accounts - .iter() - .position(|a| *a.key == *wallet_info.key) - .ok_or(ProgramError::NotEnoughAccountKeys)? as u8; - - let tlv = vec![ExtensionInstructionData::CompressedOnly( - CompressedOnlyExtensionInstructionData { - delegated_amount: 0, - withheld_transfer_fee: 0, - is_frozen: false, - compression_index: 0, - is_ata: true, - bump, - owner_index: wallet_account_index, - }, - )]; - - let source = MultiInputTokenDataWithContext { - owner: packed_ata.token_data.owner, // ATA address index - amount: packed_ata.token_data.amount, - has_delegate: packed_ata.token_data.has_delegate, - delegate: packed_ata.token_data.delegate, - mint: packed_ata.token_data.mint, - version: packed_ata.token_data.version, - merkle_context: meta.tree_info.into(), - root_index: meta.tree_info.root_index, - }; - - token_decompress_indices.push(DecompressFullIndices { - source, - destination_index: packed_ata.ata_index, - tlv: Some(tlv), - is_ata: true, - }); - } - - Ok(()) -} -``` - -### 3.2 Update function signature - -Modify `process_decompress_tokens_runtime` to accept standard ATAs: - -```rust -pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( - // ... existing params ... - ctoken_accounts: Vec<(PackedCTokenData, Meta)>, - standard_ata_accounts: Vec<(PackedStandardAtaData, Meta)>, // NEW - // ... -) -> Result<(), ProgramError> -``` - ---- - -## Phase 4: SDK Runtime Changes (sdk/src/compressible) - -### 4.1 decompress_runtime.rs - Update DecompressContext trait - -Add method to handle standard ATAs in the trait: - -```rust -/// Returns standard ATA accounts for separate processing. -fn standard_ata_accounts(&self) -> Vec<(PackedStandardAtaData, CompressedMeta)> { - Vec::new() // Default: no standard ATAs -} -``` - -### 4.2 Update process_decompress_accounts_idempotent - -Pass standard ATAs to process_tokens: - -```rust -// After collect_all_accounts returns (pdas, tokens, mints, standard_atas) -ctx.process_tokens( - // ... existing params ... - compressed_token_accounts, - standard_ata_accounts, // NEW - // ... -)?; -``` - ---- - -## Phase 5: Client Changes (compressible-client/src/lib.rs) - -### 5.1 Add StandardAtaInput struct - -```rust -/// Input for standard ATA decompression (no program-specific variant needed) -pub struct StandardAtaInput { - /// Wallet owner - MUST sign the transaction - pub wallet: Pubkey, - /// Mint for the token - pub mint: Pubkey, - /// Token data from indexer (owner = ATA address) - pub token_data: TokenData, - /// Tree info for validity proof - pub tree_info: TreeInfo, -} -``` - -### 5.2 Update decompress_accounts_idempotent signature - -```rust -pub fn decompress_accounts_idempotent( - program_id: &Pubkey, - discriminator: &[u8], - decompressed_account_addresses: &[Pubkey], - compressed_accounts: &[(CompressedAccount, T)], - standard_atas: &[StandardAtaInput], // NEW - program_account_metas: &[AccountMeta], - validity_proof_with_context: ValidityProofWithContext, -) -> Result> -``` - -### 5.3 Pack standard ATAs in instruction builder - -```rust -// Pack standard ATAs -for ata_input in standard_atas { - let (ata_address, _) = derive_ctoken_ata(&ata_input.wallet, &ata_input.mint); - - // Insert wallet as signer - remaining_accounts.insert_or_get_config(ata_input.wallet, true, false); - remaining_accounts.insert_or_get(ata_input.mint); - remaining_accounts.insert_or_get(ata_address); - - let standard_ata = StandardAtaData { - wallet: ata_input.wallet, - mint: ata_input.mint, - token_data: ata_input.token_data.clone(), - }; - let packed = standard_ata.pack(&mut remaining_accounts); - - typed_compressed_accounts.push(CompressedAccountData { - meta: /* from validity_proof_with_context */, - data: CompressedAccountVariant::PackedStandardAta(packed), - }); -} -``` - ---- - -## Phase 6: Testing - -### 6.1 Update existing test - -Modify `test_create_pdas_and_mint_auto` to: - -1. Use `StandardAtaInput` for user ATA decompression -2. Verify wallet signer requirement -3. Test mixed batch (PDAs + program tokens + standard ATAs) - -### 6.2 New test cases - -1. **Standard ATA only**: Decompress single standard ATA -2. **Mixed batch**: PDAs + CompressedMint + StandardAta + program token -3. **Signer validation**: Ensure non-signer wallet fails -4. **ATA derivation validation**: Ensure wrong wallet/mint combo fails - ---- - -## Implementation Order - -1. **ctoken-sdk/src/pack.rs** - Add data structures (30 min) -2. **macros/src/compressible/variant_enum.rs** - Add variants + trait impls (45 min) -3. **macros/src/compressible/decompress_context.rs** - Handle new variants (30 min) -4. **ctoken-sdk/src/compressible/decompress_runtime.rs** - Process standard ATAs (60 min) -5. **sdk/src/compressible/decompress_runtime.rs** - Update trait + processor (30 min) -6. **compressible-client/src/lib.rs** - Client helpers (45 min) -7. **Test updates** - Verify functionality (60 min) - -**Total estimated time: 5-6 hours** - ---- - -## Open Questions (Resolved) - -1. **Q: Should standard ATAs use a separate variant or share `PackedCTokenData`?** - **A: Separate variant (`PackedStandardAta`) for cleaner handling and explicit wallet index.** - -2. **Q: How does the client know which accounts are standard ATAs vs program tokens?** - **A: Client explicitly creates `StandardAtaInput` vs wrapping in program's `TokenAccountVariant`.** - -3. **Q: Do standard ATAs require `cmint_authority` account?** - **A: No. Standard ATAs only need wallet signer. `cmint_authority` is only for mint decompression.** - -4. **Q: Can standard ATAs be mixed with program tokens in single instruction?** - **A: Yes. All tokens (standard + program) are batched into single Transfer2 CPI.** diff --git a/sdk-libs/macros/OPTION_A_STATE_FLOW.md b/sdk-libs/macros/OPTION_A_STATE_FLOW.md deleted file mode 100644 index 5b6e879a05..0000000000 --- a/sdk-libs/macros/OPTION_A_STATE_FLOW.md +++ /dev/null @@ -1,316 +0,0 @@ -# Option A: Standard ATA/Mint Variants - State Flow Diagram - -## Overview - -Option A adds `StandardAta` and `PackedStandardAta` variants to the macro-generated `CompressedAccountVariant` enum, enabling unified decompression of arbitrary ATAs and mints alongside program-specific PDAs. - ---- - -## High-Level Decompression Flow - -``` - decompress_accounts_idempotent - | - v - +------------------------------------+ - | parse CompressedAccountData[] | - +------------------------------------+ - | - +---------------+---------------+ - | | | - v v v - +--------+ +---------+ +--------+ - | PDAs | | Tokens | | Mints | - +--------+ +---------+ +--------+ - | | | - v v v - +---------+ +---------------+ +--------+ - | CPI to | | process_tokens| | CPI to | - | Light | | _runtime | | ctoken | - | System | +---------------+ | mint | - +---------+ | +--------+ - | - +---------------------+---------------------+ - | | | - v v v - +-------------+ +-------------+ +-------------+ - | Program PDA | | StandardAta | | CompressedMint | - | Token (Vault)| | (UserAta) | | (CMint) | - +-------------+ +-------------+ +-------------+ - | | | - v v v - derive seeds derive_ctoken_ata find_mint_address - from variant (wallet, mint) (mint_seed) - | | | - v v v - CreateCToken CreateAssociated DecompressMint - AccountCpi CTokenAccountCpi Cpi - (invoke_signed) (invoke - wallet (invoke) - signs tx) -``` - ---- - -## Token Account Type Decision Tree - -``` - PackedCTokenData - | - v - +---------------------------+ - | V.is_ata() returns what? | - +---------------------------+ - | - +------------------+------------------+ - | | - is_ata = true is_ata = false - | | - v v - +----------------+ +------------------+ - | Standard ATA | | Program-owned | - | Derivation | | Token Account | - +----------------+ +------------------+ - | | - v v - derive_ctoken_ata(wallet, mint) get_seeds() from variant - wallet must be TX signer program signs via CPI - | | - v v - CreateAssociatedCTokenAccountCpi CreateTokenAccountCpi - .invoke() - no program signer .invoke_signed(&[seeds]) -``` - ---- - -## StandardAta Detailed Flow - -``` - Client Side - ----------- - StandardAtaInput { - wallet: Pubkey, // must sign TX - mint: Pubkey, - token_data: TokenData, // owner = ATA address - tree_info: TreeInfo, - } - | - v - pack_standard_ata() - | - +---> remaining_accounts.insert_or_get_config(wallet, signer=true) - +---> remaining_accounts.insert_or_get(mint) - +---> derive_ctoken_ata(wallet, mint) -> ata_address - +---> remaining_accounts.insert_or_get(ata_address) - +---> pack token_data indices - | - v - PackedStandardAtaData { - wallet_index: u8, - mint_index: u8, - ata_index: u8, - token_data: InputTokenDataCompressible, - } - | - v - CompressedAccountData { - meta: CompressedAccountMetaNoLamportsNoAddress, - data: CompressedAccountVariant::PackedStandardAta(packed), - } - - - Runtime Side - ------------ - collect_all_accounts() - | - v - match CompressedAccountVariant::PackedStandardAta(packed) - | - v - Extract to standard_ata_accounts: Vec<(PackedStandardAtaData, Meta)> - | - v - process_decompress_tokens_runtime() - | - v - for (packed_ata, meta) in standard_atas { - | - v - // 1. Validate wallet is signer - packed_accounts[wallet_index].is_signer? -> MissingRequiredSignature - | - v - // 2. Verify ATA derivation - derive_ctoken_ata(wallet, mint) == packed_accounts[ata_index]? -> InvalidAccountData - | - v - // 3. Create ATA (idempotent) - CreateAssociatedCTokenAccountCpi { - owner: wallet, - mint: mint, - bump: derived_bump, - compressible: { - compression_only: true, // ATAs must be compression_only - ... - }, - idempotent: true, - }.invoke() // No signer - wallet signs TX - | - v - // 4. Build decompress indices with TLV - DecompressFullIndices { - source: MultiInputTokenDataWithContext { - owner: ata_index, // ATA address in merkle tree - ... - }, - destination_index: ata_index, - tlv: Some([CompressedOnly { is_ata: true, owner_index: wallet_index, bump }]), - is_ata: true, - } - } - | - v - // Single Transfer2 CPI for all tokens (program PDAs + standard ATAs) - decompress_full_ctoken_accounts_with_indices(...) -``` - ---- - -## CompressedAccountVariant Enum (After Option A) - -```rust -#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] -pub enum CompressedAccountVariant { - // === Program-specific PDA variants (macro-generated) === - UserRecord(UserRecord), - PackedUserRecord(PackedUserRecord), - GameSession(GameSession), - PackedGameSession(PackedGameSession), - - // === Token variants (macro-generated) === - PackedCTokenData(PackedCTokenData), - CTokenData(CTokenData), - - // === Mint variant (existing) === - CompressedMint(CompressedMintData), - - // === NEW: Standard ATA variants (always present) === - StandardAta(StandardAtaData), - PackedStandardAta(PackedStandardAtaData), -} -``` - ---- - -## CPI Context Batching (Multi-Type Decompression) - -``` - Execution Order: PDAs -> Mints -> Tokens (tokens always last) - - Case 1: Single Type (no CPI context) - ------------------------------------ - PDAs only: LightSystemProgramCpi.invoke() - Mints only: DecompressMintCpi.invoke() - Tokens only: Transfer2 CPI invoke() - - Case 2: Multi-Type (with CPI context batching) - ---------------------------------------------- - - PDAs Mints Tokens CPI Context Action - ---- ----- ------ ------------------ - Yes No No execute directly (no context) - No Yes No execute directly (no context) - No No Yes execute directly (no context) - - Yes Yes No PDAs: first_set_context - Mints: execute (consume) - - Yes No Yes PDAs: first_set_context - Tokens: execute (consume) - - No Yes Yes Mints: first_set_context - Tokens: execute (consume) - - Yes Yes Yes PDAs: first_set_context - Mints: set_context - Tokens: execute (consume) -``` - ---- - -## Validation Rules - -### StandardAta Validation - -1. **Wallet signer check**: `remaining_accounts[wallet_index].is_signer == true` -2. **ATA derivation check**: `derive_ctoken_ata(wallet, mint) == remaining_accounts[ata_index].key` -3. **Owner consistency**: `token_data.owner` (index) points to ATA address (not wallet) - -### CompressedMint Validation - -1. **CMint derivation**: `find_mint_address(mint_seed) == cmint_pda` -2. **Authority**: fee_payer must be mint authority OR explicit cmint_authority provided - -### Program Token (Vault) Validation - -1. **PDA derivation**: `get_seeds(variant, accounts)` returns matching PDA -2. **Authority derivation**: `get_authority_seeds(variant, accounts)` returns owner PDA -3. **Program signing**: invoke_signed with seed-derived bumps - ---- - -## Account Lookup Indices - -``` -remaining_accounts layout (after system accounts): -+--------------------------------------------------------------------+ -| idx | account | usage | -+--------------------------------------------------------------------+ -| 0 | output_queue | state tree output | -| 1 | state_tree | merkle tree | -| 2 | input_queue | nullifier queue | -| ... | tree accounts | from validity proof | -| n | wallet (signer) | StandardAta wallet owner | -| n+1 | mint | token mint | -| n+2 | ATA address | derived from wallet+mint | -| n+3 | vault_authority | program-owned token authority | -| n+4 | cmint_pda | CMint address | -| ... | other accounts | | -| end | decompressed PDAs/tokens | accounts being decompressed | -+--------------------------------------------------------------------+ - -PackedStandardAtaData { - wallet_index: n, // points to signer wallet - mint_index: n+1, // points to mint - ata_index: n+2, // points to derived ATA - token_data: { - owner: n+2, // ATA address (matches compressed token owner) - mint: n+1, - amount: ..., - ... - } -} -``` - ---- - -## Files Changed Summary - -``` -sdk-libs/ - macros/src/compressible/ - variant_enum.rs # Add StandardAta, PackedStandardAta variants - decompress_context.rs # Handle StandardAta in collect_all_accounts - instructions.rs # Update collect_all_accounts helper - - ctoken-sdk/src/ - pack.rs # Add StandardAtaData, PackedStandardAtaData + Pack/Unpack - compressible/ - decompress_runtime.rs # Process standard ATAs in token flow - mod.rs # Re-export new types - - compressible-client/src/ - lib.rs # Add StandardAtaInput, update decompress helper - - sdk/src/compressible/ - decompress_runtime.rs # Pass standard ATAs to process_tokens -``` diff --git a/sdk-libs/macros/OVERVIEW.md b/sdk-libs/macros/OVERVIEW.md deleted file mode 100644 index 9aadb63e2e..0000000000 --- a/sdk-libs/macros/OVERVIEW.md +++ /dev/null @@ -1,194 +0,0 @@ -# `#[compressible]` Macro Usage Guide - -## Supported Account Types - -| Type | Description | -| ------------------------- | ----------------------------------------------------- | -| **PDAs** | Program Derived Accounts with custom seeds | -| **Program-owned CTokens** | Token accounts owned by a program PDA (vault pattern) | - ---- - -## Program-Side: Macro Syntax - -```rust -#[compressible( - // PDA: TypeName = (seeds = (...)) - UserRecord = (seeds = ("user_record", ctx.authority, data.owner)), - - // Token: TypeName = (is_token, seeds = (...), authority = (...)) - Vault = (is_token, seeds = ("vault", ctx.mint), authority = ("vault_authority")), - - // Instruction data fields used in seeds - owner = Pubkey, -)] -#[program] -pub mod my_program { ... } -``` - -### Seed Components - -| Syntax | Description | -| ---------------- | ---------------------------------------- | -| `seeds = (...)` | Required. Tuple of seed elements | -| `"literal"` | Static seed bytes (string literal) | -| `b"literal"` | Static seed bytes (byte string literal) | -| `CONST` | Crate-level constant (`&str` or `&[u8]`) | -| `ctx.account` | Account from instruction context | -| `data.field` | Field from instruction data | -| `is_token` | Marks account as CToken (not PDA) | -| `authority = ()` | (tokens only) PDA that owns the token | - -**Constants:** Uppercase identifiers are resolved as `crate::CONST` and support both `&str` and `&[u8]`: - -```rust -pub const MY_SEED: &str = "my_seed"; // &str constant -pub const MY_BYTES: &[u8] = b"my_bytes"; // &[u8] constant - -#[compressible( - MyAccount = (seeds = (MY_SEED, ctx.user)), - MyOther = (seeds = (MY_BYTES, ctx.user)), -)] -``` - ---- - -## Generated Code - -The macro generates: - -1. **`CompressedAccountVariant`** - enum with all PDA types + token variants -2. **`TokenAccountVariant`** - enum for token account types -3. **`DecompressAccountsIdempotent`** - Anchor accounts struct -4. **`CompressAccountsIdempotent`** - Anchor accounts struct -5. **`SeedParams`** - struct for `data.*` seed fields -6. **`TokenSeedProvider`** impl - derives token seeds -7. **`PdaSeedDerivation`** impl - derives PDA seeds -8. **`DecompressContext`** impl - runtime decompression logic -9. **`decompress_accounts_idempotent()`** - instruction handler -10. **`compress_accounts_idempotent()`** - instruction handler - ---- - -## Client-Side Usage - -### Building Decompress Instruction - -```rust -use light_compressible_client::compressible_instruction; - -// 1. Fetch compressed accounts from indexer -let compressed_user = indexer.get_compressed_account(user_hash).await?; -let compressed_vault = indexer.get_compressed_account(vault_hash).await?; - -// 2. Get validity proof -let proof = indexer.get_validity_proof( - vec![compressed_user.hash, compressed_vault.hash], - vec![], - None, -).await?; - -// 3. Build instruction -let instruction = compressible_instruction::decompress_accounts_idempotent( - &program_id, - &DECOMPRESS_DISCRIMINATOR, - &[user_pda, vault_pda], // Target on-chain addresses - &[ - (compressed_user, user_data), // PDAs first - (compressed_vault, token_data), // Tokens after - ], - &program_accounts.to_account_metas(None), - proof, -)?; - -// 4. Append SeedParams if needed -let seed_params = SeedParams { owner }; -instruction.data.extend_from_slice(&borsh::to_vec(&seed_params)?); -``` - -### Account Ordering - -When mixing PDAs and tokens, order matters for CPI context: - -```rust -// Correct: PDAs first, tokens after -&[ - (compressed_pda, pda_data), - (compressed_token, token_data), -] -``` - ---- - -## CPI Context Rules - -When decompressing **both PDAs and tokens** in one instruction: - -1. PDAs **write** to CPI context first -2. Tokens **execute** (consume CPI context) last -3. CPI context validation checks: `cpi_context.associated_tree == first_input.tree` **at execution time** - -**Critical:** The client uses the **first token's** `cpi_context`, not the first PDA's: - -```rust -// In compressible-client (already handled internally): -// Uses first TOKEN's tree context since tokens execute last -let first_token_cpi_context = compressed_accounts - .iter() - .find(|(acc, _)| acc.owner == LIGHT_TOKEN_PROGRAM_ID) - .map(|(acc, _)| acc.tree_info.cpi_context.unwrap()); -``` - ---- - -## Example: Full Program - -```rust -use anchor_lang::prelude::*; -use light_sdk_macros::compressible; - -/// Seed constants - both &str and &[u8] are supported -pub const PROFILE_SEED: &str = "profile"; -pub const VAULT_SEED: &[u8] = b"vault"; - -#[compressible( - // PDA with &str constant - UserProfile = (seeds = (PROFILE_SEED, ctx.authority, data.user_id)), - - // Token with &[u8] constant - UserVault = (is_token, seeds = (VAULT_SEED, ctx.mint), authority = ("vault_auth", ctx.authority)), - - // Seed params - user_id = [u8; 32], -)] -#[program] -pub mod my_program { - use super::*; - - pub fn create_profile(ctx: Context, user_id: [u8; 32]) -> Result<()> { - // ... create compressed profile - Ok(()) - } - - // decompress_accounts_idempotent is auto-generated - // compress_accounts_idempotent is auto-generated -} - -#[derive(Accounts)] -pub struct CreateProfile<'info> { - #[account(mut)] - pub authority: Signer<'info>, - pub mint: Account<'info, Mint>, -} -``` - ---- - -## Key Files - -| File | Purpose | -| -------------------------------- | ------------------------------- | -| `macros/src/compressible/` | Macro implementation | -| `sdk/src/compressible/` | Runtime traits & PDA processing | -| `ctoken-sdk/src/compressible/` | Token decompression runtime | -| `compressible-client/src/lib.rs` | Client instruction builders | diff --git a/sdk-libs/macros/SPEC_OPTION_A.md b/sdk-libs/macros/SPEC_OPTION_A.md deleted file mode 100644 index a8fe485677..0000000000 --- a/sdk-libs/macros/SPEC_OPTION_A.md +++ /dev/null @@ -1,546 +0,0 @@ -# SPEC: Option A - Standard Variants in Macro-Generated Enum - -## Overview - -Add `StandardAta` and `StandardMint` as always-present variants in the macro-generated `TokenAccountVariant` enum. Programs automatically get these standard variants without declaration. - -## Goals - -1. Enable decompression of arbitrary ATAs and Mints without per-program customization -2. Use fixed, known data structures and derivation logic -3. Maintain single unified enum for all account types -4. Zero breaking changes to existing programs that don't use standard types - ---- - -## Data Structures - -### StandardAtaData (New) - -```rust -/// Standard ATA data for decompression. -/// Compressed TokenData.owner = ATA address (NOT wallet). -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct StandardAtaData { - /// Wallet owner pubkey - MUST be a signer on the transaction. - /// The ATA is derived from (wallet, light_token_program_id, mint). - pub wallet: Pubkey, - /// Mint pubkey for this token account. - pub mint: Pubkey, - /// Token data from compressed account. - /// CRITICAL: token_data.owner = ATA address (not wallet). - pub token_data: TokenData, -} -``` - -### PackedStandardAtaData (New) - -```rust -/// Packed StandardAtaData with indices into remaining_accounts. -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct PackedStandardAtaData { - /// Index of wallet in remaining_accounts (must be signer). - pub wallet_index: u8, - /// Index of mint in remaining_accounts. - pub mint_index: u8, - /// Index of ATA address in remaining_accounts (same as token_data.owner). - pub ata_index: u8, - /// Packed token data (owner/delegate/mint are indices). - pub token_data: InputTokenDataCompressible, -} -``` - -### StandardMintData (Existing CompressedMintData - Reuse) - -```rust -/// Already exists in ctoken-sdk/src/pack.rs as CompressedMintData. -/// Rename/alias to StandardMintData for clarity. -pub type StandardMintData = CompressedMintData; - -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct CompressedMintData { - /// Mint seed pubkey (used to derive CMint PDA via find_mint_address). - pub mint_seed_pubkey: Pubkey, - /// Compressed mint with context (from indexer). - pub compressed_mint_with_context: CompressedMintWithContext, - /// Rent payment in epochs (must be >= 2). - pub rent_payment: u8, - /// Lamports for future write operations. - pub write_top_up: u32, -} -``` - ---- - -## Enum Changes - -### TokenAccountVariant (Modified) - -The macro will always generate these standard variants: - -```rust -// sdk-libs/macros/src/compressible/seed_providers.rs -pub fn generate_ctoken_account_variant_enum(specs: &[TokenSeedSpec]) -> Result { - // ... existing program-specific variants ... - - quote! { - #[derive(Clone, Copy, Debug, AnchorSerialize, AnchorDeserialize, PartialEq, Eq)] - pub enum TokenAccountVariant { - // Program-specific variants (from macro args) - #(#program_variants,)* - - // Standard variants (always present) - /// Standard ATA - uses fixed derivation (wallet, light_token_program, mint). - StandardAta, - /// Standard Mint - uses fixed derivation find_mint_address(mint_seed). - StandardMint, - } - } -} -``` - -### CompressedAccountVariant (Modified) - -```rust -// sdk-libs/macros/src/compressible/variant_enum.rs -pub enum CompressedAccountVariant { - // Program PDA variants - #(#account_variants)* - - // Token variants - PackedCTokenData(PackedCTokenData), - CTokenData(CTokenData), - - // Mint variant (existing) - CompressedMint(CompressedMintData), - - // NEW: Standard ATA variant (separate from CTokenData for cleaner handling) - StandardAta(StandardAtaData), - PackedStandardAta(PackedStandardAtaData), -} -``` - ---- - -## Trait Implementations - -### TokenSeedProvider for StandardAta - -```rust -impl TokenSeedProvider for TokenAccountVariant { - // ... existing match arms ... - - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [AccountInfo<'info>], - ) -> Result<(Vec>, Pubkey), ProgramError> { - match self { - // ... existing program-specific arms ... - - TokenAccountVariant::StandardAta => { - // StandardAta doesn't use program seeds - derivation is fixed. - // Return empty seeds; the runtime handles ATA creation separately. - Err(ProgramError::InvalidArgument) // Should not be called - } - TokenAccountVariant::StandardMint => { - // StandardMint doesn't use program seeds - derivation is fixed. - Err(ProgramError::InvalidArgument) // Should not be called - } - } - } - - fn get_authority_seeds<'a, 'info>(...) -> Result<...> { - match self { - TokenAccountVariant::StandardAta => { - Err(ProgramError::InvalidArgument) // ATAs don't need authority seeds - } - TokenAccountVariant::StandardMint => { - Err(ProgramError::InvalidArgument) // Mints don't need authority seeds for decompress - } - // ... existing arms ... - } - } - - fn is_ata(&self) -> bool { - matches!(self, TokenAccountVariant::StandardAta) - } -} -``` - -### HasTokenVariant Updates - -```rust -impl HasTokenVariant for CompressedAccountData { - fn is_packed_ctoken(&self) -> bool { - matches!( - self.data, - CompressedAccountVariant::PackedCTokenData(_) - | CompressedAccountVariant::PackedStandardAta(_) - ) - } - - fn is_compressed_mint(&self) -> bool { - matches!(self.data, CompressedAccountVariant::CompressedMint(_)) - } - - fn is_standard_ata(&self) -> bool { - matches!( - self.data, - CompressedAccountVariant::StandardAta(_) - | CompressedAccountVariant::PackedStandardAta(_) - ) - } -} -``` - -### Pack/Unpack for StandardAtaData - -```rust -impl Pack for StandardAtaData { - type Packed = PackedStandardAtaData; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - // Derive ATA address from wallet + mint - let (ata_address, _bump) = derive_ctoken_ata(&self.wallet, &self.mint); - - // Insert all required accounts - let wallet_index = remaining_accounts.insert_or_get_config(self.wallet, true, false); // signer - let mint_index = remaining_accounts.insert_or_get(self.mint); - let ata_index = remaining_accounts.insert_or_get(ata_address); - - // Pack token data - let token_data = self.token_data.pack(remaining_accounts); - - PackedStandardAtaData { - wallet_index, - mint_index, - ata_index, - token_data, - } - } -} - -impl Unpack for PackedStandardAtaData { - type Unpacked = StandardAtaData; - - fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result { - let wallet = *remaining_accounts - .get(self.wallet_index as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)? - .key; - let mint = *remaining_accounts - .get(self.mint_index as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)? - .key; - let token_data = self.token_data.unpack(remaining_accounts)?; - - Ok(StandardAtaData { wallet, mint, token_data }) - } -} -``` - ---- - -## Runtime Processing - -### process_decompress_tokens_runtime (Modified) - -```rust -// sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs - -pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( - // ... existing params ... - // ADD: standard ATAs - standard_atas: Vec<(PackedStandardAtaData, CompressedAccountMetaNoLamportsNoAddress)>, -) -> Result<(), ProgramError> { - // ... existing token processing ... - - // Process standard ATAs - for (packed_ata, meta) in standard_atas.into_iter() { - let wallet_info = &packed_accounts[packed_ata.wallet_index as usize]; - let mint_info = &packed_accounts[packed_ata.mint_index as usize]; - let ata_info = &packed_accounts[packed_ata.ata_index as usize]; - - // Verify wallet is signer - if !wallet_info.is_signer { - msg!("StandardAta wallet must be signer: {:?}", wallet_info.key); - return Err(ProgramError::MissingRequiredSignature); - } - - // Verify ATA derivation - let (derived_ata, bump) = derive_ctoken_ata(wallet_info.key, mint_info.key); - if derived_ata != *ata_info.key { - msg!("ATA derivation mismatch: derived={:?}, provided={:?}", derived_ata, ata_info.key); - return Err(ProgramError::InvalidAccountData); - } - - // Create ATA if needed (idempotent) - CreateAssociatedCTokenAccountCpi { - payer: fee_payer.clone(), - associated_token_account: ata_info.clone(), - owner: wallet_info.clone(), - mint: mint_info.clone(), - system_program: cpi_accounts.system_program()?.clone(), - bump, - compressible: CompressibleParamsCpi { - compressible_config: ctoken_config.clone(), - rent_sponsor: ctoken_rent_sponsor.clone(), - system_program: cpi_accounts.system_program()?.clone(), - pre_pay_num_epochs: 2, - lamports_per_write: None, - compress_to_account_pubkey: None, - token_account_version: TokenDataVersion::ShaFlat, - compression_only: true, // ATAs require compression_only - }, - idempotent: true, - }.invoke()?; - - // Build decompress indices - let owner_index = packed_ata.token_data.owner; // ATA address index - let wallet_account_index = packed_ata.wallet_index; - - let source = MultiInputTokenDataWithContext { - owner: owner_index, - amount: packed_ata.token_data.amount, - has_delegate: packed_ata.token_data.has_delegate, - delegate: packed_ata.token_data.delegate, - mint: packed_ata.token_data.mint, - version: packed_ata.token_data.version, - merkle_context: meta.tree_info.into(), - root_index: meta.tree_info.root_index, - }; - - let tlv = vec![ExtensionInstructionData::CompressedOnly( - CompressedOnlyExtensionInstructionData { - delegated_amount: 0, - withheld_transfer_fee: 0, - is_frozen: false, - compression_index: 0, - is_ata: true, - bump, - owner_index: wallet_account_index, - }, - )]; - - let decompress_index = DecompressFullIndices { - source, - destination_index: packed_ata.ata_index, - tlv: Some(tlv), - is_ata: true, - }; - token_decompress_indices.push(decompress_index); - } - - // ... rest of existing logic (single Transfer2 CPI) ... -} -``` - ---- - -## collect_all_accounts (Modified) - -```rust -// Macro-generated in __macro_helpers module - -fn collect_all_accounts<'a, 'b, 'info>( - // ... existing params ... -) -> Result<( - Vec, // PDAs - Vec<(PackedCTokenData, Meta)>, // Program tokens - Vec<(CompressedMintData, Meta)>, // Mints - Vec<(PackedStandardAtaData, Meta)>, // NEW: Standard ATAs -), ProgramError> { - // ... existing setup ... - - let mut standard_ata_accounts = Vec::new(); - - for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { - let meta = compressed_data.meta; - match compressed_data.data { - // ... existing PDA arms ... - - CompressedAccountVariant::PackedCTokenData(data) => { - compressed_token_accounts.push((data, meta)); - } - CompressedAccountVariant::CompressedMint(data) => { - compressed_mint_accounts.push((data, meta)); - } - - // NEW: Standard ATA handling - CompressedAccountVariant::PackedStandardAta(data) => { - standard_ata_accounts.push((data, meta)); - } - CompressedAccountVariant::StandardAta(_) => { - unreachable!("Unpacked StandardAta should not appear"); - } - - // ... other arms ... - } - } - - Ok((compressed_pda_infos, compressed_token_accounts, compressed_mint_accounts, standard_ata_accounts)) -} -``` - ---- - -## Client-Side Changes - -### compressible_instruction::decompress_accounts_idempotent (Modified) - -```rust -// sdk-libs/compressible-client/src/lib.rs - -pub fn decompress_accounts_idempotent( - program_id: &Pubkey, - discriminator: &[u8], - decompressed_account_addresses: &[Pubkey], - compressed_accounts: &[(CompressedAccount, T)], - // NEW: Standard ATAs - standard_atas: &[StandardAtaInput], - // NEW: Standard Mints - standard_mints: &[StandardMintInput], - program_account_metas: &[AccountMeta], - validity_proof_with_context: ValidityProofWithContext, -) -> Result> -where - T: Pack + Clone + std::fmt::Debug, -{ - // ... existing setup ... - - // Pack standard ATAs - for ata_input in standard_atas { - let (ata_address, _) = derive_ctoken_ata(&ata_input.wallet, &ata_input.mint); - remaining_accounts.insert_or_get_config(ata_input.wallet, true, false); // signer - remaining_accounts.insert_or_get(ata_input.mint); - remaining_accounts.insert_or_get(ata_address); - - // Build StandardAtaData and pack - let standard_ata = StandardAtaData { - wallet: ata_input.wallet, - mint: ata_input.mint, - token_data: ata_input.token_data.clone(), - }; - let packed = standard_ata.pack(&mut remaining_accounts); - - typed_compressed_accounts.push(CompressedAccountData { - meta: /* from validity_proof_with_context */, - data: CompressedAccountVariant::PackedStandardAta(packed), - }); - } - - // Pack standard mints - for mint_input in standard_mints { - let (cmint_address, _) = find_mint_address(&mint_input.mint_seed); - remaining_accounts.insert_or_get(mint_input.mint_seed); - remaining_accounts.insert_or_get(cmint_address); - - typed_compressed_accounts.push(CompressedAccountData { - meta: /* from validity_proof_with_context */, - data: CompressedAccountVariant::CompressedMint(CompressedMintData { - mint_seed_pubkey: mint_input.mint_seed, - compressed_mint_with_context: mint_input.compressed_mint_with_context.clone(), - rent_payment: mint_input.rent_payment, - write_top_up: mint_input.write_top_up, - }), - }); - } - - // ... rest of instruction building ... -} - -/// Input for standard ATA decompression -pub struct StandardAtaInput { - pub wallet: Pubkey, // Must be tx signer - pub mint: Pubkey, - pub token_data: TokenData, // owner = ATA address - pub tree_info: TreeInfo, -} - -/// Input for standard mint decompression -pub struct StandardMintInput { - pub mint_seed: Pubkey, - pub compressed_mint_with_context: CompressedMintWithContext, - pub rent_payment: u8, - pub write_top_up: u32, - pub tree_info: TreeInfo, -} -``` - ---- - -## DecompressAccountsIdempotent Accounts (Modified) - -```rust -// Macro-generated accounts struct - -#[derive(Accounts)] -pub struct DecompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - - /// Program's compressible config - pub config: AccountInfo<'info>, - - /// Program's rent sponsor - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - - // CToken accounts - REQUIRED if any tokens/ATAs/mints present - /// CToken rent sponsor (ctoken program's rent sponsor PDA) - #[account(mut)] - pub ctoken_rent_sponsor: Option>, - - /// CToken compressible config - pub ctoken_config: Option>, - - /// CToken program - pub light_token_program: Option>, - - /// CToken CPI authority - pub ctoken_cpi_authority: Option>, - - // ... other optional accounts for program-specific seeds ... -} -``` - -The runtime will validate that ctoken accounts are Some when standard ATAs/mints are present. - ---- - -## Validation Rules - -1. **Standard ATA validation:** - - `wallet` must be a signer in the transaction - - `derive_ctoken_ata(wallet, mint)` must equal the ATA destination account - - `token_data.owner` (ATA address) must match derived ATA - -2. **Standard Mint validation:** - - `find_mint_address(mint_seed)` must equal the CMint destination account - - No signature required (mint authority doesn't need to sign for decompress) - -3. **Account requirements:** - - If any standard ATAs or mints present: ctoken_config, ctoken_rent_sponsor, light_token_program, ctoken_cpi_authority must be Some - - If only PDAs: ctoken accounts can be None - ---- - -## Files to Modify - -1. `sdk-libs/macros/src/compressible/seed_providers.rs` - Add StandardAta/StandardMint variants -2. `sdk-libs/macros/src/compressible/variant_enum.rs` - Add StandardAta variant handling -3. `sdk-libs/macros/src/compressible/instructions.rs` - Update collect_all_accounts -4. `sdk-libs/macros/src/compressible/decompress_context.rs` - Update trait impl -5. `sdk-libs/ctoken-sdk/src/pack.rs` - Add StandardAtaData, PackedStandardAtaData -6. `sdk-libs/ctoken-sdk/src/compressible/decompress_runtime.rs` - Handle standard ATAs -7. `sdk-libs/sdk/src/compressible/decompress_runtime.rs` - Update process_decompress_accounts_idempotent -8. `sdk-libs/compressible-client/src/lib.rs` - Add standard ATA/mint params - ---- - -## Migration Path - -1. Existing programs: No changes required, StandardAta/StandardMint variants available automatically -2. New programs: Can use standard types without declaring in macro -3. Tests: Update to use new client helper signature diff --git a/sdk-libs/macros/SPEC_OPTION_B.md b/sdk-libs/macros/SPEC_OPTION_B.md deleted file mode 100644 index 0ea38914c8..0000000000 --- a/sdk-libs/macros/SPEC_OPTION_B.md +++ /dev/null @@ -1,796 +0,0 @@ -# SPEC: Option B - Separate Standard Types from Instruction Data - -## Overview - -Introduce separate instruction data fields for standard ATAs and Mints, completely decoupled from the program-specific `CompressedAccountVariant` enum. Clean architectural separation. - -## Goals - -1. Complete decoupling of standard types from program enum -2. Client can pass any number of ATAs/Mints without knowing program internals -3. Clean, auditable separation of concerns -4. Fully standardized handling with no program-specific code paths - ---- - -## Instruction Data Format (Breaking Change) - -### Current Format - -```rust -pub struct DecompressMultipleAccountsIdempotentData { - pub proof: ValidityProof, - pub compressed_accounts: Vec>, - pub system_accounts_offset: u8, -} -``` - -### New Format - -```rust -/// New instruction data format with separate fields for standard types. -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct DecompressAccountsIdempotentData { - /// Validity proof covering ALL accounts (PDAs + ATAs + Mints). - pub proof: ValidityProof, - - /// Program-specific compressed accounts (PDAs and program-owned tokens). - pub compressed_accounts: Vec>, - - /// Standard ATAs - fixed derivation, wallet signs. - pub standard_atas: Vec, - - /// Standard Mints - fixed derivation, no signature required. - pub standard_mints: Vec, - - /// Offset to system accounts in remaining_accounts. - pub system_accounts_offset: u8, -} -``` - ---- - -## Data Structures - -### StandardAtaData (Client-Side) - -```rust -/// Standard ATA data for client-side instruction building. -/// Location: sdk-libs/compressible-client/src/types.rs (NEW FILE) -#[derive(Clone, Debug)] -pub struct StandardAtaData { - /// Wallet owner - MUST be transaction signer. - pub wallet: Pubkey, - /// Mint pubkey. - pub mint: Pubkey, - /// Token data from indexer. CRITICAL: token_data.owner = ATA address. - pub token_data: TokenData, - /// Tree info from indexer. - pub tree_info: TreeInfo, -} -``` - -### PackedStandardAtaData (Serialized) - -```rust -/// Packed StandardAta for on-chain deserialization. -/// Location: sdk-libs/sdk/src/compressible/standard_types.rs (NEW FILE) -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct PackedStandardAtaData { - /// Index of wallet in remaining_accounts (must be signer). - pub wallet_index: u8, - /// Index of mint in remaining_accounts. - pub mint_index: u8, - /// Index of ATA destination in remaining_accounts. - pub ata_destination_index: u8, - /// Packed token data (indices into remaining_accounts). - pub token_data: PackedTokenData, - /// Compressed account metadata. - pub meta: CompressedAccountMetaNoLamportsNoAddress, -} - -/// Minimal packed token data for standard ATAs. -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct PackedTokenData { - /// Index of owner (ATA address) in remaining_accounts. - pub owner_index: u8, - /// Index of mint in remaining_accounts. - pub mint_index: u8, - /// Token amount. - pub amount: u64, - /// Has delegate flag. - pub has_delegate: bool, - /// Delegate index (0 if no delegate). - pub delegate_index: u8, - /// Token data version (3 = ShaFlat). - pub version: u8, -} -``` - -### StandardMintData (Client-Side) - -```rust -/// Standard Mint data for client-side instruction building. -#[derive(Clone, Debug)] -pub struct StandardMintData { - /// Mint seed pubkey (derives CMint via find_mint_address). - pub mint_seed: Pubkey, - /// Compressed mint with context from indexer. - pub compressed_mint_with_context: CompressedMintWithContext, - /// Rent payment in epochs (>= 2). - pub rent_payment: u8, - /// Lamports for future writes. - pub write_top_up: u32, - /// Tree info from indexer. - pub tree_info: TreeInfo, -} -``` - -### PackedStandardMintData (Serialized) - -```rust -/// Packed StandardMint for on-chain deserialization. -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct PackedStandardMintData { - /// Index of mint_seed in remaining_accounts. - pub mint_seed_index: u8, - /// Index of CMint destination in remaining_accounts. - pub cmint_destination_index: u8, - /// Compressed mint with context. - pub compressed_mint_with_context: CompressedMintWithContext, - /// Rent payment in epochs. - pub rent_payment: u8, - /// Write top-up lamports. - pub write_top_up: u32, - /// Compressed account metadata. - pub meta: CompressedAccountMetaNoLamportsNoAddress, -} -``` - ---- - -## Runtime Processing - -### process_decompress_accounts_idempotent (Modified) - -```rust -// sdk-libs/sdk/src/compressible/decompress_runtime.rs - -#[inline(never)] -#[allow(clippy::too_many_arguments)] -pub fn process_decompress_accounts_idempotent<'info, Ctx>( - ctx: &Ctx, - remaining_accounts: &[AccountInfo<'info>], - compressed_accounts: Vec, - standard_atas: Vec, // NEW - standard_mints: Vec, // NEW - proof: ValidityProof, - system_accounts_offset: u8, - cpi_signer: CpiSigner, - program_id: &Pubkey, - seed_params: Option<&Ctx::SeedParams>, -) -> Result<(), ProgramError> -where - Ctx: DecompressContext<'info>, -{ - // Determine what types we have - let has_program_accounts = !compressed_accounts.is_empty(); - let has_standard_atas = !standard_atas.is_empty(); - let has_standard_mints = !standard_mints.is_empty(); - - // Check ctoken accounts required - if (has_standard_atas || has_standard_mints) { - // Validate ctoken accounts are present - ctx.ctoken_config().ok_or_else(|| { - msg!("ctoken_config required for standard ATAs/Mints"); - ProgramError::NotEnoughAccountKeys - })?; - ctx.ctoken_rent_sponsor().ok_or_else(|| { - msg!("ctoken_rent_sponsor required for standard ATAs/Mints"); - ProgramError::NotEnoughAccountKeys - })?; - } - - // Count types for CPI context batching - let (has_tokens, has_pdas, has_mints) = check_account_types(&compressed_accounts); - let has_any_tokens = has_tokens || has_standard_atas; - let has_any_mints = has_mints || has_standard_mints; - - let type_count = has_any_tokens as u8 + has_pdas as u8 + has_any_mints as u8; - let needs_cpi_context = type_count >= 2; - - // ... setup CPI accounts ... - - // 1. Process PDAs (if any) - from compressed_accounts - let (compressed_pda_infos, compressed_token_accounts, program_mint_accounts) = - ctx.collect_all_accounts(...)?; - - if !compressed_pda_infos.is_empty() { - // ... existing PDA processing with CPI context ... - } - - // 2. Process Mints (standard + program-specific) - let all_mints: Vec<_> = standard_mints - .into_iter() - .map(|m| (m.into_compressed_mint_data(), m.meta)) - .chain(program_mint_accounts) - .collect(); - - if !all_mints.is_empty() { - process_all_mints( - ctx, - &cpi_accounts, - all_mints, - proof, - has_pdas, // has_prior_context - has_any_tokens, // has_subsequent - )?; - } - - // 3. Process Tokens (standard ATAs + program-specific) - if has_any_tokens { - process_all_tokens( - ctx, - remaining_accounts, - compressed_token_accounts, // program-specific - standard_atas, // standard ATAs - proof, - &cpi_accounts, - has_pdas || has_any_mints, // has_prior_context - program_id, - )?; - } - - Ok(()) -} -``` - -### process_standard_atas (New Function) - -```rust -// sdk-libs/ctoken-sdk/src/compressible/standard_ata.rs (NEW FILE) - -/// Process standard ATAs in unified flow. -/// Handles ATA creation (idempotent) and builds decompress indices. -#[inline(never)] -pub fn process_standard_atas<'info>( - standard_atas: Vec, - packed_accounts: &[AccountInfo<'info>], - fee_payer: &AccountInfo<'info>, - ctoken_config: &AccountInfo<'info>, - ctoken_rent_sponsor: &AccountInfo<'info>, - system_program: &AccountInfo<'info>, - decompress_indices: &mut Vec, -) -> Result<(), ProgramError> { - for packed_ata in standard_atas { - // Get accounts from indices - let wallet_info = packed_accounts - .get(packed_ata.wallet_index as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let mint_info = packed_accounts - .get(packed_ata.mint_index as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let ata_info = packed_accounts - .get(packed_ata.ata_destination_index as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - - // CRITICAL: Verify wallet is signer - if !wallet_info.is_signer { - msg!("StandardAta: wallet must be signer: {:?}", wallet_info.key); - return Err(ProgramError::MissingRequiredSignature); - } - - // Derive and verify ATA address - let (derived_ata, bump) = derive_ctoken_ata(wallet_info.key, mint_info.key); - if derived_ata != *ata_info.key { - msg!( - "StandardAta: derivation mismatch. wallet={:?}, mint={:?}, expected={:?}, got={:?}", - wallet_info.key, mint_info.key, derived_ata, ata_info.key - ); - return Err(ProgramError::InvalidAccountData); - } - - // Verify token_data.owner matches ATA address - let owner_info = packed_accounts - .get(packed_ata.token_data.owner_index as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - if *owner_info.key != derived_ata { - msg!( - "StandardAta: token_data.owner must equal ATA address. owner={:?}, ata={:?}", - owner_info.key, derived_ata - ); - return Err(ProgramError::InvalidAccountData); - } - - // Create ATA (idempotent) - CreateAssociatedCTokenAccountCpi { - payer: fee_payer.clone(), - associated_token_account: ata_info.clone(), - owner: wallet_info.clone(), - mint: mint_info.clone(), - system_program: system_program.clone(), - bump, - compressible: CompressibleParamsCpi { - compressible_config: ctoken_config.clone(), - rent_sponsor: ctoken_rent_sponsor.clone(), - system_program: system_program.clone(), - pre_pay_num_epochs: 2, - lamports_per_write: None, - compress_to_account_pubkey: None, - token_account_version: TokenDataVersion::ShaFlat, - compression_only: true, - }, - idempotent: true, - }.invoke()?; - - // Build decompress indices - let source = MultiInputTokenDataWithContext { - owner: packed_ata.token_data.owner_index, - amount: packed_ata.token_data.amount, - has_delegate: packed_ata.token_data.has_delegate, - delegate: packed_ata.token_data.delegate_index, - mint: packed_ata.token_data.mint_index, - version: packed_ata.token_data.version, - merkle_context: packed_ata.meta.tree_info.into(), - root_index: packed_ata.meta.tree_info.root_index, - }; - - // Build TLV for ATA - let tlv = vec![ExtensionInstructionData::CompressedOnly( - CompressedOnlyExtensionInstructionData { - delegated_amount: 0, - withheld_transfer_fee: 0, - is_frozen: false, - compression_index: 0, - is_ata: true, - bump, - owner_index: packed_ata.wallet_index, - }, - )]; - - decompress_indices.push(DecompressFullIndices { - source, - destination_index: packed_ata.ata_destination_index, - tlv: Some(tlv), - is_ata: true, - }); - } - - Ok(()) -} -``` - -### process_standard_mints (New Function) - -```rust -// sdk-libs/ctoken-sdk/src/compressible/standard_mint.rs (NEW FILE) - -/// Process standard mints via CPI to ctoken program. -#[inline(never)] -pub fn process_standard_mints<'info>( - standard_mints: Vec, - packed_accounts: &[AccountInfo<'info>], - fee_payer: &AccountInfo<'info>, - cpi_accounts: &CpiAccounts<'_, 'info>, - ctoken_config: &AccountInfo<'info>, - ctoken_rent_sponsor: &AccountInfo<'info>, - ctoken_cpi_authority: &AccountInfo<'info>, - proof: ValidityProof, - has_prior_context: bool, - has_subsequent: bool, -) -> Result<(), ProgramError> { - if standard_mints.is_empty() { - return Ok(()); - } - - let mint_count = standard_mints.len(); - let last_idx = mint_count - 1; - - let mints_only = !has_prior_context && !has_subsequent; - let cpi_context_account = if mints_only { - None - } else { - Some(cpi_accounts.cpi_context()?.clone()) - }; - - // Build system accounts once - let system_accounts = SystemAccountInfos { - light_system_program: cpi_accounts.get_account_info(0)?.clone(), - cpi_authority_pda: cpi_accounts.authority()?.clone(), - registered_program_pda: cpi_accounts.registered_program_pda()?.clone(), - account_compression_authority: cpi_accounts.account_compression_authority()?.clone(), - account_compression_program: cpi_accounts.account_compression_program()?.clone(), - system_program: cpi_accounts.system_program()?.clone(), - }; - - let state_tree = cpi_accounts.get_tree_account_info(0)?; - let input_queue = cpi_accounts.get_tree_account_info(1)?; - let output_queue = cpi_accounts.get_tree_account_info(2)?; - - for (idx, packed_mint) in standard_mints.into_iter().enumerate() { - // Get accounts from indices - let mint_seed_info = packed_accounts - .get(packed_mint.mint_seed_index as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let cmint_info = packed_accounts - .get(packed_mint.cmint_destination_index as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - - // Verify CMint derivation - let (derived_cmint, _) = find_mint_address(mint_seed_info.key); - if derived_cmint != *cmint_info.key { - msg!( - "StandardMint: derivation mismatch. mint_seed={:?}, expected={:?}, got={:?}", - mint_seed_info.key, derived_cmint, cmint_info.key - ); - return Err(ProgramError::InvalidAccountData); - } - - if mints_only { - // Direct execution - DecompressMintCpi { - mint_seed: mint_seed_info.clone(), - authority: fee_payer.clone(), // No authority check for decompress - payer: fee_payer.clone(), - cmint: cmint_info.clone(), - compressible_config: ctoken_config.clone(), - rent_sponsor: ctoken_rent_sponsor.clone(), - state_tree: state_tree.clone(), - input_queue: input_queue.clone(), - output_queue: output_queue.clone(), - system_accounts: system_accounts.clone(), - compressed_mint_with_context: packed_mint.compressed_mint_with_context, - proof: ValidityProof(proof.0), - rent_payment: packed_mint.rent_payment, - write_top_up: packed_mint.write_top_up, - }.invoke()?; - } else { - // CPI context batching - let is_first = !has_prior_context && idx == 0; - let is_last = idx == last_idx; - let should_execute = is_last && !has_subsequent; - - let cpi_ctx = if should_execute { - CpiContext { first_set_context: false, set_context: false, ..Default::default() } - } else if is_first { - CpiContext { first_set_context: true, set_context: false, ..Default::default() } - } else { - CpiContext { first_set_context: false, set_context: true, ..Default::default() } - }; - - DecompressCMintCpiWithContext { - mint_seed: mint_seed_info.clone(), - authority: fee_payer.clone(), - payer: fee_payer.clone(), - cmint: cmint_info.clone(), - compressible_config: ctoken_config.clone(), - rent_sponsor: ctoken_rent_sponsor.clone(), - state_tree: state_tree.clone(), - input_queue: input_queue.clone(), - output_queue: output_queue.clone(), - cpi_context_account: cpi_context_account.as_ref().unwrap().clone(), - system_accounts: system_accounts.clone(), - ctoken_cpi_authority: ctoken_cpi_authority.clone(), - compressed_mint_with_context: packed_mint.compressed_mint_with_context, - proof: ValidityProof(proof.0), - rent_payment: packed_mint.rent_payment, - write_top_up: packed_mint.write_top_up, - cpi_context: cpi_ctx, - }.invoke()?; - } - } - - Ok(()) -} -``` - ---- - -## Client-Side Changes - -### decompress_accounts_idempotent (Rewritten) - -```rust -// sdk-libs/compressible-client/src/lib.rs - -/// Build decompress_accounts_idempotent instruction with separate standard types. -#[allow(clippy::too_many_arguments)] -pub fn decompress_accounts_idempotent( - program_id: &Pubkey, - discriminator: &[u8], - // Program-specific accounts - decompressed_pda_addresses: &[Pubkey], - compressed_accounts: &[(CompressedAccount, T)], - // Standard types (NEW) - standard_atas: &[StandardAtaData], - standard_mints: &[StandardMintData], - // Accounts - program_account_metas: &[AccountMeta], - validity_proof_with_context: ValidityProofWithContext, -) -> Result> -where - T: Pack + Clone + std::fmt::Debug, -{ - let mut remaining_accounts = PackedAccounts::default(); - - // Determine if we need CPI context - let has_pdas = !compressed_accounts.is_empty(); - let has_tokens_or_atas = compressed_accounts.iter().any(|(ca, _)| ca.owner == LIGHT_TOKEN_PROGRAM_ID.into()) - || !standard_atas.is_empty(); - let has_mints = !standard_mints.is_empty(); - - let needs_cpi_context = (has_pdas as u8 + has_tokens_or_atas as u8 + has_mints as u8) >= 2; - - // Setup system accounts - if needs_cpi_context { - let cpi_context = compressed_accounts.first() - .or_else(|| standard_atas.first().map(|_| /* get from proof */)) - .or_else(|| standard_mints.first().map(|_| /* get from proof */)) - .ok_or("No accounts to process")? - .0.tree_info.cpi_context.unwrap(); - - remaining_accounts.add_system_accounts_v2( - SystemAccountMetaConfig::new_with_cpi_context(*program_id, cpi_context) - )?; - } else { - remaining_accounts.add_system_accounts_v2( - SystemAccountMetaConfig::new(*program_id) - )?; - } - - // Pack output queue - let output_queue = get_output_queue(&validity_proof_with_context.accounts[0].tree_info); - let output_state_tree_index = remaining_accounts.insert_or_get(output_queue); - - // Pack tree infos - let packed_tree_infos = validity_proof_with_context.pack_tree_infos(&mut remaining_accounts); - - // 1. Pack program-specific compressed accounts - let mut typed_compressed_accounts = Vec::new(); - for (i, (compressed_account, data)) in compressed_accounts.iter().enumerate() { - remaining_accounts.insert_or_get(compressed_account.tree_info.queue); - let tree_info = packed_tree_infos.state_trees.as_ref().unwrap().packed_tree_infos[i]; - let packed_data = data.pack(&mut remaining_accounts); - - typed_compressed_accounts.push(CompressedAccountData { - meta: CompressedAccountMetaNoLamportsNoAddress { tree_info, output_state_tree_index }, - data: packed_data, - }); - } - - // 2. Pack standard ATAs - let mut packed_standard_atas = Vec::new(); - for ata in standard_atas { - // Derive ATA address - let (ata_address, _) = derive_ctoken_ata(&ata.wallet, &ata.mint); - - // Insert accounts (wallet as signer) - let wallet_index = remaining_accounts.insert_or_get_config(ata.wallet, true, false); - let mint_index = remaining_accounts.insert_or_get(ata.mint); - let ata_destination_index = remaining_accounts.insert_or_get(ata_address); - - // Pack token data - // CRITICAL: token_data.owner = ATA address (from compressed account) - let owner_index = remaining_accounts.insert_or_get(ata.token_data.owner); // ATA address - let delegate_index = ata.token_data.delegate - .map(|d| remaining_accounts.insert_or_get(d)) - .unwrap_or(0); - - // Get tree info for this account from validity proof - let tree_info_idx = compressed_accounts.len() + packed_standard_atas.len(); - let tree_info = packed_tree_infos.state_trees.as_ref().unwrap().packed_tree_infos[tree_info_idx]; - - packed_standard_atas.push(PackedStandardAtaData { - wallet_index, - mint_index, - ata_destination_index, - token_data: PackedTokenData { - owner_index, - mint_index, - amount: ata.token_data.amount, - has_delegate: ata.token_data.delegate.is_some(), - delegate_index, - version: 3, // ShaFlat - }, - meta: CompressedAccountMetaNoLamportsNoAddress { tree_info, output_state_tree_index }, - }); - } - - // 3. Pack standard mints - let mut packed_standard_mints = Vec::new(); - for mint in standard_mints { - let (cmint_address, _) = find_mint_address(&mint.mint_seed); - - let mint_seed_index = remaining_accounts.insert_or_get(mint.mint_seed); - let cmint_destination_index = remaining_accounts.insert_or_get(cmint_address); - - let tree_info_idx = compressed_accounts.len() + packed_standard_atas.len() + packed_standard_mints.len(); - let tree_info = packed_tree_infos.state_trees.as_ref().unwrap().packed_tree_infos[tree_info_idx]; - - packed_standard_mints.push(PackedStandardMintData { - mint_seed_index, - cmint_destination_index, - compressed_mint_with_context: mint.compressed_mint_with_context.clone(), - rent_payment: mint.rent_payment, - write_top_up: mint.write_top_up, - meta: CompressedAccountMetaNoLamportsNoAddress { tree_info, output_state_tree_index }, - }); - } - - // Build accounts - let mut accounts = program_account_metas.to_vec(); - let (system_accounts, system_accounts_offset, _) = remaining_accounts.to_account_metas(); - accounts.extend(system_accounts); - - // Add PDA destination accounts - for pda in decompressed_pda_addresses { - accounts.push(AccountMeta::new(*pda, false)); - } - - // Add ATA destination accounts - for ata in standard_atas { - let (ata_address, _) = derive_ctoken_ata(&ata.wallet, &ata.mint); - accounts.push(AccountMeta::new(ata_address, false)); - } - - // Add CMint destination accounts - for mint in standard_mints { - let (cmint_address, _) = find_mint_address(&mint.mint_seed); - accounts.push(AccountMeta::new(cmint_address, false)); - } - - // Serialize instruction data - let instruction_data = DecompressAccountsIdempotentData { - proof: validity_proof_with_context.proof, - compressed_accounts: typed_compressed_accounts, - standard_atas: packed_standard_atas, - standard_mints: packed_standard_mints, - system_accounts_offset: system_accounts_offset as u8, - }; - - let serialized = instruction_data.try_to_vec()?; - let mut data = Vec::with_capacity(discriminator.len() + serialized.len()); - data.extend_from_slice(discriminator); - data.extend_from_slice(&serialized); - - Ok(Instruction { - program_id: *program_id, - accounts, - data, - }) -} -``` - ---- - -## Macro Changes - -### Instruction Handler (Modified) - -```rust -// sdk-libs/macros/src/compressible/instructions.rs - -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, - standard_atas: Vec, // NEW - standard_mints: Vec, // NEW - system_accounts_offset: u8, - #seed_params - ) -> Result<()> { - __processor_functions::process_decompress_accounts_idempotent( - &ctx.accounts, - &ctx.remaining_accounts, - proof, - compressed_accounts, - standard_atas, // NEW - standard_mints, // NEW - system_accounts_offset, - #seed_args - ) - } - }) -} -``` - -### Processor Function (Modified) - -```rust -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, - standard_atas: Vec, - standard_mints: Vec, - system_accounts_offset: u8, - #params - ) -> Result<()> { - light_sdk::compressible::process_decompress_accounts_idempotent( - accounts, - remaining_accounts, - compressed_accounts, - standard_atas, - standard_mints, - proof, - system_accounts_offset, - LIGHT_CPI_SIGNER, - &crate::ID, - #seed_params_arg, - ) - .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) - } - }) -} -``` - ---- - -## Accounts Struct (Same as Option A) - -```rust -#[derive(Accounts)] -pub struct DecompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - pub config: AccountInfo<'info>, - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - - // Required when standard ATAs or Mints present - #[account(mut)] - pub ctoken_rent_sponsor: Option>, - pub ctoken_config: Option>, - pub light_token_program: Option>, - pub ctoken_cpi_authority: Option>, - - // ... program-specific optional accounts ... -} -``` - ---- - -## Validation Rules - -Same as Option A: - -1. **Standard ATA validation:** - - `wallet` must be signer - - `derive_ctoken_ata(wallet, mint) == ata_destination` - - `token_data.owner == ata_destination` (ATA address) - -2. **Standard Mint validation:** - - `find_mint_address(mint_seed) == cmint_destination` - - No signature required - -3. **Account requirements:** - - Standard types present => ctoken accounts required - ---- - -## Files to Modify - -1. `sdk-libs/sdk/src/compressible/mod.rs` - Export new types -2. `sdk-libs/sdk/src/compressible/standard_types.rs` (NEW) - PackedStandardAtaData, PackedStandardMintData -3. `sdk-libs/sdk/src/compressible/decompress_runtime.rs` - New signature, delegate to standard handlers -4. `sdk-libs/ctoken-sdk/src/compressible/mod.rs` - Export new functions -5. `sdk-libs/ctoken-sdk/src/compressible/standard_ata.rs` (NEW) - process_standard_atas -6. `sdk-libs/ctoken-sdk/src/compressible/standard_mint.rs` (NEW) - process_standard_mints -7. `sdk-libs/compressible-client/src/lib.rs` - New instruction builder -8. `sdk-libs/compressible-client/src/types.rs` (NEW) - StandardAtaData, StandardMintData -9. `sdk-libs/macros/src/compressible/instructions.rs` - New params in generated code - ---- - -## Migration - -1. All existing callers must update to new instruction format -2. Tests need to pass empty vecs for standard_atas/standard_mints if not using -3. No backward compatibility - clean break diff --git a/sdk-libs/macros/src/compressible/anchor_seeds.rs b/sdk-libs/macros/src/compressible/anchor_seeds.rs index 5c32c820e8..f80596a6eb 100644 --- a/sdk-libs/macros/src/compressible/anchor_seeds.rs +++ b/sdk-libs/macros/src/compressible/anchor_seeds.rs @@ -3,8 +3,6 @@ //! This module extracts PDA seeds from Anchor's attribute syntax and classifies them //! into the categories needed for compression: literals, ctx fields, data fields, etc. -use proc_macro2::TokenStream; -use quote::quote; use syn::{Expr, Ident, ItemStruct, Type}; /// Classified seed element from Anchor's seeds array @@ -33,14 +31,10 @@ pub enum ClassifiedSeed { /// Extracted seed specification for a compressible field #[derive(Clone, Debug)] pub struct ExtractedSeedSpec { - /// The field name in the Accounts struct - pub field_name: Ident, /// The variant name derived from field_name (snake_case -> CamelCase) pub variant_name: Ident, /// The inner type (e.g., UserRecord from Account<'info, UserRecord>) pub inner_type: Ident, - /// Whether it's Box> - pub is_boxed: bool, /// Classified seeds from #[account(seeds = [...])] pub seeds: Vec, } @@ -66,8 +60,6 @@ pub struct ExtractedAccountsInfo { pub struct_name: Ident, pub pda_fields: Vec, pub token_fields: Vec, - /// All fields in the struct (for authority lookup) - pub all_fields: Vec<(Ident, Type)>, } /// Extract rentfree field info from an Accounts struct @@ -121,11 +113,10 @@ pub fn extract_from_accounts_struct( Ident::new(&camel, field_ident.span()) }; + let _ = (field_ident, is_boxed); // Suppress unused warnings pda_fields.push(ExtractedSeedSpec { - field_name: field_ident, variant_name, inner_type, - is_boxed, seeds, }); } else if let Some(token_attr) = token_attr { @@ -188,11 +179,11 @@ pub fn extract_from_accounts_struct( } } + let _ = all_fields; // Suppress unused warning Ok(Some(ExtractedAccountsInfo { struct_name: item.ident.clone(), pda_fields, token_fields, - all_fields, })) } @@ -649,70 +640,6 @@ fn extract_ctx_ident_from_expr(expr: &Expr) -> Option { } } -/// Generate seed derivation code from classified seeds -pub fn generate_seed_derivation(seeds: &[ClassifiedSeed]) -> TokenStream { - let seed_exprs: Vec = seeds - .iter() - .map(|seed| match seed { - ClassifiedSeed::Literal(bytes) => { - quote! { &[#(#bytes),*] } - } - ClassifiedSeed::Constant(path) => { - quote! { crate::#path.as_ref() } - } - ClassifiedSeed::CtxAccount(ident) => { - quote! { ctx_seeds.#ident.as_ref() } - } - ClassifiedSeed::DataField { - field_name, - conversion: None, - } => { - quote! { self.#field_name.as_ref() } - } - ClassifiedSeed::DataField { - field_name, - conversion: Some(method), - } => { - quote! { self.#field_name.#method().as_ref() } - } - ClassifiedSeed::FunctionCall { func, ctx_args } => { - let args: Vec = ctx_args - .iter() - .map(|arg| quote! { &ctx_seeds.#arg }) - .collect(); - quote! { #func(#(#args),*).as_ref() } - } - }) - .collect(); - - quote! { - let seeds: &[&[u8]] = &[#(#seed_exprs),*]; - } -} - -/// Get ctx field names from classified seeds -pub fn get_ctx_fields(seeds: &[ClassifiedSeed]) -> Vec { - let mut fields = Vec::new(); - for seed in seeds { - match seed { - ClassifiedSeed::CtxAccount(ident) => { - if !fields.iter().any(|f: &Ident| f == ident) { - fields.push(ident.clone()); - } - } - ClassifiedSeed::FunctionCall { ctx_args, .. } => { - for arg in ctx_args { - if !fields.iter().any(|f: &Ident| f == arg) { - fields.push(arg.clone()); - } - } - } - _ => {} - } - } - fields -} - /// Get data field names from classified seeds pub fn get_data_fields(seeds: &[ClassifiedSeed]) -> Vec<(Ident, Option)> { let mut fields = Vec::new(); diff --git a/sdk-libs/macros/src/compressible/decompress_context.rs b/sdk-libs/macros/src/compressible/decompress_context.rs index fdf5f9dda1..cdccdfe8a3 100644 --- a/sdk-libs/macros/src/compressible/decompress_context.rs +++ b/sdk-libs/macros/src/compressible/decompress_context.rs @@ -20,13 +20,11 @@ pub fn generate_decompress_context_trait_impl( let packed_name = format_ident!("Packed{}", pda_type); let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", pda_type); let ctx_fields = &info.ctx_seed_fields; - // Generate pattern to extract idx fields from packed variant let idx_field_patterns: Vec<_> = ctx_fields.iter().map(|field| { let idx_field = format_ident!("{}_idx", field); quote! { #idx_field } }).collect(); - // Generate code to resolve idx fields to Pubkeys let resolve_ctx_seeds: Vec<_> = ctx_fields.iter().map(|field| { let idx_field = format_ident!("{}_idx", field); @@ -37,7 +35,6 @@ pub fn generate_decompress_context_trait_impl( .key; } }).collect(); - // Generate CtxSeeds struct construction let ctx_seeds_construction = if ctx_fields.is_empty() { quote! { let ctx_seeds = #ctx_seeds_struct_name; } @@ -47,7 +44,6 @@ pub fn generate_decompress_context_trait_impl( }).collect(); quote! { let ctx_seeds = #ctx_seeds_struct_name { #(#field_inits),* }; } }; - if ctx_fields.is_empty() { quote! { RentFreeAccountVariant::#packed_name { data: packed, .. } => { @@ -106,7 +102,7 @@ pub fn generate_decompress_context_trait_impl( .collect(); let packed_token_variant_ident = format_ident!("Packed{}", token_variant_ident); - + Ok(quote! { impl<#lifetime> light_sdk::compressible::DecompressContext<#lifetime> for DecompressAccountsIdempotent<#lifetime> { type CompressedData = RentFreeAccountData; diff --git a/sdk-libs/macros/src/compressible/file_scanner.rs b/sdk-libs/macros/src/compressible/file_scanner.rs index e9300b07ba..ea52500298 100644 --- a/sdk-libs/macros/src/compressible/file_scanner.rs +++ b/sdk-libs/macros/src/compressible/file_scanner.rs @@ -4,6 +4,7 @@ //! from Accounts structs that contain #[rentfree] fields. use std::path::{Path, PathBuf}; + use syn::{Item, ItemMod, ItemStruct}; use crate::compressible::anchor_seeds::{ diff --git a/sdk-libs/macros/src/compressible/instructions.rs b/sdk-libs/macros/src/compressible/instructions.rs index 47c91da186..50265641ae 100644 --- a/sdk-libs/macros/src/compressible/instructions.rs +++ b/sdk-libs/macros/src/compressible/instructions.rs @@ -276,7 +276,9 @@ fn extract_ctx_account_fields(seed: &SeedElement) -> Vec { /// Extract all ctx.accounts.XXX field names from a list of seed elements. /// Deduplicates the fields. -pub fn extract_ctx_seed_fields(seeds: &syn::punctuated::Punctuated) -> Vec { +pub fn extract_ctx_seed_fields( + seeds: &syn::punctuated::Punctuated, +) -> Vec { let mut all_fields = Vec::new(); for seed in seeds { all_fields.extend(extract_ctx_account_fields(seed)); @@ -329,7 +331,9 @@ fn extract_data_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { } /// Phase 5: Extract all data.XXX field names from a list of seed elements. -pub fn extract_data_seed_fields(seeds: &syn::punctuated::Punctuated) -> Vec { +pub fn extract_data_seed_fields( + seeds: &syn::punctuated::Punctuated, +) -> Vec { let mut all_fields = Vec::new(); for seed in seeds { if let SeedElement::Expression(expr) = seed { @@ -651,8 +655,10 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( } syn::Expr::MethodCall(method_call) => { let mut new_method_call = method_call.clone(); - new_method_call.receiver = - Box::new(map_pda_expr_to_ctx_seeds(&method_call.receiver, ctx_field_names)); + new_method_call.receiver = Box::new(map_pda_expr_to_ctx_seeds( + &method_call.receiver, + ctx_field_names, + )); new_method_call.args = method_call .args .iter() @@ -890,7 +896,7 @@ fn convert_classified_to_seed_elements( seeds: &[crate::compressible::anchor_seeds::ClassifiedSeed], ) -> Punctuated { use crate::compressible::anchor_seeds::ClassifiedSeed; - + let mut result = Punctuated::new(); for seed in seeds { let elem = match seed { @@ -913,18 +919,25 @@ fn convert_classified_to_seed_elements( let expr: Expr = syn::parse_quote!(ctx.#ident); SeedElement::Expression(Box::new(expr)) } - ClassifiedSeed::DataField { field_name, conversion: None } => { + ClassifiedSeed::DataField { + field_name, + conversion: None, + } => { let expr: Expr = syn::parse_quote!(data.#field_name); SeedElement::Expression(Box::new(expr)) } - ClassifiedSeed::DataField { field_name, conversion: Some(method) } => { + ClassifiedSeed::DataField { + field_name, + conversion: Some(method), + } => { let expr: Expr = syn::parse_quote!(data.#field_name.#method()); SeedElement::Expression(Box::new(expr)) } ClassifiedSeed::FunctionCall { func, ctx_args } => { - let args: Vec = ctx_args.iter().map(|arg| { - syn::parse_quote!(&ctx.#arg.key()) - }).collect(); + let args: Vec = ctx_args + .iter() + .map(|arg| syn::parse_quote!(&ctx.#arg.key())) + .collect(); let expr: Expr = syn::parse_quote!(#func(#(#args),*)); SeedElement::Expression(Box::new(expr)) } @@ -937,7 +950,9 @@ fn convert_classified_to_seed_elements( fn convert_classified_to_seed_elements_vec( seeds: &[crate::compressible::anchor_seeds::ClassifiedSeed], ) -> Vec { - convert_classified_to_seed_elements(seeds).into_iter().collect() + convert_classified_to_seed_elements(seeds) + .into_iter() + .collect() } /// Generate all code from extracted seeds (shared logic with add_compressible_instructions) @@ -1009,7 +1024,9 @@ fn generate_from_extracted_seeds( .map(|spec| (spec.field_name.to_string(), &spec.field_type)) .collect(); - let seeds_structs_and_constructors: Vec = if let Some(ref pda_seed_specs) = pda_seeds { + let seeds_structs_and_constructors: Vec = if let Some(ref pda_seed_specs) = + pda_seeds + { pda_seed_specs .iter() .zip(pda_ctx_seeds.iter()) @@ -1017,12 +1034,10 @@ fn generate_from_extracted_seeds( let type_name = &spec.variant; let seeds_struct_name = format_ident!("{}Seeds", type_name); let constructor_name = format_ident!("{}", to_snake_case(&type_name.to_string())); - let ctx_fields = &ctx_info.ctx_seed_fields; let ctx_field_decls: Vec<_> = ctx_fields.iter().map(|field| { quote! { pub #field: solana_pubkey::Pubkey } }).collect(); - let data_fields = extract_data_seed_fields(&spec.seeds); let data_field_decls: Vec<_> = data_fields.iter().filter_map(|field| { let field_str = field.to_string(); @@ -1030,7 +1045,6 @@ fn generate_from_extracted_seeds( quote! { pub #field: #ty } }) }).collect(); - let data_verifications: Vec<_> = data_fields.iter().map(|field| { quote! { if data.#field != seeds.#field { @@ -1038,14 +1052,12 @@ fn generate_from_extracted_seeds( } } }).collect(); - quote! { #[derive(Clone, Debug)] pub struct #seeds_struct_name { #(#ctx_field_decls,)* #(#data_field_decls,)* } - impl RentFreeAccountVariant { pub fn #constructor_name( account_data: &[u8], @@ -1053,16 +1065,15 @@ fn generate_from_extracted_seeds( ) -> std::result::Result { use anchor_lang::AnchorDeserialize; let data = #type_name::deserialize(&mut &account_data[..])?; - + #(#data_verifications)* - + std::result::Result::Ok(Self::#type_name { data, #(#ctx_fields: seeds.#ctx_fields,)* }) } } - impl light_sdk::compressible::IntoVariant for #seeds_struct_name { fn into_variant(self, data: &[u8]) -> std::result::Result { RentFreeAccountVariant::#constructor_name(data, self) @@ -1108,13 +1119,13 @@ fn generate_from_extracted_seeds( } else { return Err(macro_error!(name, "No seed specifications provided")); }; - + let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", name); 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)] @@ -1128,11 +1139,11 @@ fn generate_from_extracted_seeds( } } }; - + let seed_derivation = generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, &instruction_data, ctx_fields)?; Ok(quote! { #ctx_seeds_struct - + impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, ()> for #name { fn derive_pda_seeds_with_accounts( &self, @@ -1167,8 +1178,10 @@ fn generate_from_extracted_seeds( pda_ctx_seeds.clone(), token_variant_name, )?; - let decompress_processor_fn = generate_process_decompress_accounts_idempotent(instruction_variant, &instruction_data)?; - let decompress_instruction = generate_decompress_instruction_entrypoint(instruction_variant, &instruction_data)?; + let decompress_processor_fn = + generate_process_decompress_accounts_idempotent(instruction_variant, &instruction_data)?; + let decompress_instruction = + generate_decompress_instruction_entrypoint(instruction_variant, &instruction_data)?; let compress_accounts: syn::ItemStruct = match instruction_variant { InstructionVariant::PdaOnly => unreachable!(), @@ -1190,7 +1203,8 @@ fn generate_from_extracted_seeds( }, }; - let compress_context_impl = generate_compress_context_impl(instruction_variant, account_types.clone())?; + let compress_context_impl = + generate_compress_context_impl(instruction_variant, account_types.clone())?; let compress_processor_fn = generate_process_compress_accounts_idempotent(instruction_variant)?; let compress_instruction = generate_compress_instruction_entrypoint(instruction_variant)?; @@ -1330,7 +1344,10 @@ fn generate_from_extracted_seeds( // Add ctoken seed provider impl if let Some(ref seeds) = token_seeds { if !seeds.is_empty() { - let impl_code = crate::compressible::seed_providers::generate_ctoken_seed_provider_implementation(seeds)?; + let impl_code = + crate::compressible::seed_providers::generate_ctoken_seed_provider_implementation( + seeds, + )?; let ctoken_impl: syn::ItemImpl = syn::parse2(impl_code)?; content.1.push(Item::Impl(ctoken_impl)); } @@ -1393,7 +1410,9 @@ fn extract_context_and_params(fn_item: &syn::ItemFn) -> Option<(String, Ident)> if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { // Find the last type argument (T or T<'info>) for arg in args.args.iter().rev() { - if let syn::GenericArgument::Type(syn::Type::Path(inner_path)) = arg { + if let syn::GenericArgument::Type(syn::Type::Path(inner_path)) = + arg + { if let Some(inner_seg) = inner_path.path.segments.last() { context_type = Some(inner_seg.ident.to_string()); break; @@ -1460,14 +1479,12 @@ fn wrap_function_with_rentfree(fn_item: &syn::ItemFn, params_ident: &Ident) -> s wrapped } - #[inline(never)] -pub fn compressible_program_impl( - _args: TokenStream, - mut module: ItemMod, -) -> Result { - use crate::compressible::anchor_seeds::get_data_fields; - use crate::compressible::file_scanner::{resolve_crate_src_path, scan_module_for_compressible}; +pub fn compressible_program_impl(_args: TokenStream, mut module: ItemMod) -> Result { + use crate::compressible::{ + anchor_seeds::get_data_fields, + file_scanner::{resolve_crate_src_path, scan_module_for_compressible}, + }; if module.content.is_none() { return Err(macro_error!(&module, "Module must have a body")); diff --git a/sdk-libs/macros/src/compressible/light_compressible.rs b/sdk-libs/macros/src/compressible/light_compressible.rs index 5471c0a7b6..95f38f8fe5 100644 --- a/sdk-libs/macros/src/compressible/light_compressible.rs +++ b/sdk-libs/macros/src/compressible/light_compressible.rs @@ -112,9 +112,10 @@ fn derive_input_to_item_struct(input: &DeriveInput) -> Result { #[cfg(test)] mod tests { - use super::*; use syn::parse_quote; + use super::*; + #[test] fn test_light_compressible_basic() { // No #[hash] or #[skip] needed - SHA256 hashes entire struct, compression_info auto-skipped @@ -133,10 +134,7 @@ mod tests { let output = result.unwrap().to_string(); // Should contain LightHasherSha output - assert!( - output.contains("DataHasher"), - "Should implement DataHasher" - ); + assert!(output.contains("DataHasher"), "Should implement DataHasher"); assert!( output.contains("ToByteArray"), "Should implement ToByteArray" @@ -157,10 +155,7 @@ mod tests { output.contains("HasCompressionInfo"), "Should implement HasCompressionInfo" ); - assert!( - output.contains("CompressAs"), - "Should implement CompressAs" - ); + assert!(output.contains("CompressAs"), "Should implement CompressAs"); assert!(output.contains("Size"), "Should implement Size"); // Should contain CompressiblePack output (Pack, Unpack, Packed struct) @@ -195,10 +190,7 @@ mod tests { let output = result.unwrap().to_string(); // compress_as attribute should be processed - assert!( - output.contains("CompressAs"), - "Should implement CompressAs" - ); + assert!(output.contains("CompressAs"), "Should implement CompressAs"); } #[test] @@ -220,10 +212,7 @@ mod tests { let output = result.unwrap().to_string(); // Should still generate everything - assert!( - output.contains("DataHasher"), - "Should implement DataHasher" - ); + assert!(output.contains("DataHasher"), "Should implement DataHasher"); assert!( output.contains("LightDiscriminator"), "Should implement LightDiscriminator" @@ -247,10 +236,7 @@ mod tests { }; let result = derive_light_compressible(input); - assert!( - result.is_err(), - "LightCompressible should fail for enums" - ); + assert!(result.is_err(), "LightCompressible should fail for enums"); } #[test] diff --git a/sdk-libs/macros/src/compressible/seed_providers.rs b/sdk-libs/macros/src/compressible/seed_providers.rs index 604cf8c1e3..b2d54de2cc 100644 --- a/sdk-libs/macros/src/compressible/seed_providers.rs +++ b/sdk-libs/macros/src/compressible/seed_providers.rs @@ -2,7 +2,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{spanned::Spanned, Ident, Result}; +use syn::{Ident, Result}; use crate::compressible::instructions::{InstructionDataSpec, SeedElement, TokenSeedSpec}; @@ -44,10 +44,9 @@ fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { | "rent_sponsor" | "config" | "compression_authority" - ) { - if seen.insert(field_name_str) { - ctx_fields.push(field_name.clone()); - } + ) && seen.insert(field_name_str) + { + ctx_fields.push(field_name.clone()); } } } @@ -63,10 +62,9 @@ fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { if !matches!( field_name_str.as_str(), "fee_payer" | "rent_sponsor" | "config" | "compression_authority" - ) { - if seen.insert(field_name_str) { - ctx_fields.push(field_name.clone()); - } + ) && seen.insert(field_name_str) + { + ctx_fields.push(field_name.clone()); } } } diff --git a/sdk-libs/macros/src/finalize/codegen.rs b/sdk-libs/macros/src/finalize/codegen.rs index 8cd36926a2..5cc3a93f86 100644 --- a/sdk-libs/macros/src/finalize/codegen.rs +++ b/sdk-libs/macros/src/finalize/codegen.rs @@ -12,10 +12,11 @@ //! 2. Instruction body: Can use hot CMint (mintTo, transfers, etc.) //! 3. Finalize: No-op (all work done in pre_init) -use super::parse::{ParsedCompressibleStruct, RentFreeField}; use proc_macro2::TokenStream; use quote::{format_ident, quote}; +use super::parse::{ParsedCompressibleStruct, RentFreeField}; + /// Generate both trait implementations pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream { let struct_name = &parsed.struct_name; @@ -173,7 +174,7 @@ pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream /// Generate LightPreInit body for PDAs + mints: /// 1. Write PDAs to CPI context /// 2. Invoke mint_action with decompress + CPI context -/// After this, CMint is "hot" and usable in instruction body +/// After this, Mint is "hot" and usable in instruction body #[allow(clippy::too_many_arguments)] fn generate_pre_init_pdas_and_mints( parsed: &ParsedCompressibleStruct, diff --git a/sdk-libs/macros/src/finalize/parse.rs b/sdk-libs/macros/src/finalize/parse.rs index 3167f30846..869073ba13 100644 --- a/sdk-libs/macros/src/finalize/parse.rs +++ b/sdk-libs/macros/src/finalize/parse.rs @@ -212,12 +212,12 @@ fn extract_account_type(ty: &Type) -> Option<(bool, &syn::Path)> { if ident_str == "Box" { // Check for Box> if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { - if let Type::Path(inner_path) = inner_ty { - if let Some(inner_seg) = inner_path.path.segments.last() { - if inner_seg.ident == "Account" { - return Some((true, &inner_path.path)); - } + if let Some(syn::GenericArgument::Type(Type::Path(inner_path))) = + args.args.first() + { + if let Some(inner_seg) = inner_path.path.segments.last() { + if inner_seg.ident == "Account" { + return Some((true, &inner_path.path)); } } } @@ -275,7 +275,7 @@ pub fn parse_compressible_struct(input: &DeriveInput) -> Result( - remaining_accounts: &[AccountInfo<'info>], +pub fn process_decompress_tokens_runtime<'info, 'b, V>( + _remaining_accounts: &[AccountInfo<'info>], fee_payer: &AccountInfo<'info>, token_program: &AccountInfo<'info>, token_rent_sponsor: &AccountInfo<'info>, @@ -59,8 +57,7 @@ where crate::compressed_token::decompress_full::DecompressFullIndices, > = Vec::with_capacity(token_accounts.len()); // Only program-owned tokens need signer seeds - let mut token_signers_seed_groups: Vec>> = - Vec::with_capacity(token_accounts.len()); + let mut token_signers_seed_groups: Vec>> = Vec::with_capacity(token_accounts.len()); let packed_accounts = post_system_accounts; // CPI context usage for token decompression: diff --git a/sdk-libs/token-sdk/src/pack.rs b/sdk-libs/token-sdk/src/pack.rs index 277158035e..43b0810b4d 100644 --- a/sdk-libs/token-sdk/src/pack.rs +++ b/sdk-libs/token-sdk/src/pack.rs @@ -218,7 +218,7 @@ pub mod compat { } #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] - pub struct PackedCTokenDataWithVariant { + pub struct PackedTokenDataWithVariant { pub variant: V, pub token_data: InputTokenDataCompressible, } @@ -234,10 +234,10 @@ pub mod compat { V: Pack, V::Packed: AnchorSerialize + Clone + std::fmt::Debug, { - type Packed = PackedCTokenDataWithVariant; + type Packed = PackedTokenDataWithVariant; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - PackedCTokenDataWithVariant { + PackedTokenDataWithVariant { variant: self.variant.pack(remaining_accounts), token_data: self.token_data.pack(remaining_accounts), } @@ -255,7 +255,7 @@ pub mod compat { remaining_accounts: &[AccountInfo], ) -> std::result::Result { // Note: This impl assumes V is already unpacked (has Pubkeys). - // For packed variants, use PackedCTokenDataWithVariant::unpack instead. + // For packed variants, use PackedTokenDataWithVariant::unpack instead. Ok(TokenDataWithVariant { variant: self.variant.clone(), token_data: self.token_data.unpack(remaining_accounts)?, @@ -268,17 +268,17 @@ pub mod compat { V: Pack, V::Packed: AnchorSerialize + Clone + std::fmt::Debug, { - type Packed = PackedCTokenDataWithVariant; + type Packed = PackedTokenDataWithVariant; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - PackedCTokenDataWithVariant { + PackedTokenDataWithVariant { variant: self.variant.pack(remaining_accounts), token_data: self.token_data.pack(remaining_accounts), } } } - impl Unpack for PackedCTokenDataWithVariant + impl Unpack for PackedTokenDataWithVariant where V: Unpack, { @@ -299,7 +299,7 @@ pub mod compat { pub type InputTokenDataCompressible = light_token_interface::instructions::transfer2::MultiTokenTransferOutputData; pub type CompressibleTokenDataWithVariant = CTokenDataWithVariant; - pub type PackedCompressibleTokenDataWithVariant = PackedCTokenDataWithVariant; + pub type PackedCompressibleTokenDataWithVariant = PackedTokenDataWithVariant; pub type CTokenData = CTokenDataWithVariant; - pub type PackedCTokenData = PackedCTokenDataWithVariant; + pub type PackedCTokenData = PackedTokenDataWithVariant; } diff --git a/sdk-libs/token-sdk/src/token/mod.rs b/sdk-libs/token-sdk/src/token/mod.rs index 3cca9ce39e..e016cad387 100644 --- a/sdk-libs/token-sdk/src/token/mod.rs +++ b/sdk-libs/token-sdk/src/token/mod.rs @@ -120,8 +120,10 @@ pub use burn_checked::*; pub use close::{CloseAccount, CloseAccountCpi}; pub use compressible::{CompressibleParams, CompressibleParamsCpi}; pub use create::*; -pub use create_ata::CreateCTokenAtaCpi as CreateAssociatedAccountCpi; -pub use create_ata::{derive_token_ata, CreateAssociatedTokenAccount, CreateCTokenAtaCpi}; +pub use create_ata::{ + derive_token_ata, CreateAssociatedTokenAccount, + CreateCTokenAtaCpi as CreateAssociatedAccountCpi, CreateCTokenAtaCpi, +}; pub use create_mint::*; pub use decompress::Decompress; pub use decompress_mint::*; diff --git a/sdk-libs/token-sdk/tests/pack_test.rs b/sdk-libs/token-sdk/tests/pack_test.rs index 8c80a2889e..7c52d45c53 100644 --- a/sdk-libs/token-sdk/tests/pack_test.rs +++ b/sdk-libs/token-sdk/tests/pack_test.rs @@ -2,7 +2,7 @@ use light_sdk::instruction::PackedAccounts; use light_token_sdk::{ - compat::{PackedTokenDataWithVariant, TokenData, TokenDataWithVariant}, + compat::{PackedCompressibleTokenDataWithVariant, TokenData, TokenDataWithVariant}, pack::Pack, }; use solana_pubkey::Pubkey; @@ -52,6 +52,14 @@ fn test_token_data_with_variant_packing() { TypeB = 1, } + impl Pack for MyVariant { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + *self + } + } + let mut remaining_accounts = PackedAccounts::default(); let token_with_variant = TokenDataWithVariant { @@ -67,8 +75,8 @@ fn test_token_data_with_variant_packing() { }; // Pack the wrapper - let packed: PackedTokenDataWithVariant = - token_with_variant.pack(&mut remaining_accounts).unwrap(); + let packed: PackedCompressibleTokenDataWithVariant = + token_with_variant.pack(&mut remaining_accounts); // Verify variant is unchanged assert!(matches!(packed.variant, MyVariant::TypeA)); diff --git a/sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md b/sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md deleted file mode 100644 index 70f40852ba..0000000000 --- a/sdk-tests/csdk-anchor-full-derived-test/SUMMARY.md +++ /dev/null @@ -1,208 +0,0 @@ -# Light Protocol: Atomic PDA + Mint Creation via Macros - -## Overview - -This test program demonstrates how `#[compressible]` PDAs and `#[light_mint]` can be combined in a single instruction with a single proof, enabling atomic creation of compressed accounts and decompressed mints. - -## Key Components - -### 1. Macro Attributes - -**`#[compressible]`** - Applied to PDA account fields: - -- `address_tree_info`: Packed address tree info from params -- `output_tree`: State tree index for compressed account output - -**`#[light_mint]`** - Applied to mint placeholder fields: - -- `mint_signer`: PDA that derives the CMint address -- `authority`: Mint authority (must be signer) -- `decimals`: Mint decimals -- `address_tree_info`: Address tree info for mint's compressed address -- `signer_seeds`: Optional seeds for PDA signing - -### 2. Derive Macros - -**`#[derive(LightFinalize)]`** - Implements `LightPreInit` and `LightFinalize` traits: - -- Detects `#[compressible]` and `#[light_mint]` fields -- Auto-detects ctoken accounts: `ctoken_compressible_config`, `ctoken_rent_sponsor`, `light_token_program`, `ctoken_cpi_authority` - -**`#[light_instruction(params)]`** - Wraps instruction handlers: - -- Calls `light_pre_init()` BEFORE instruction body (all compression logic here) -- Calls `light_finalize()` AFTER instruction body (no-op) - -### 3. Execution Flow (PDAs + Mint) - -``` -Instruction Entry - | - v -light_pre_init() - | - +---> 1. Build CpiAccounts with CPI context - | - +---> 2. Prepare compressed account infos for all PDAs (with_data=false) - | - +---> 3. write_to_cpi_context_first() - Write PDAs to CPI context - | - +---> 4. Build MintActionCompressedInstructionData - | - CreateMint with compressed address - | - DecompressMintAction (creates CMint on-chain) - | - CpiContext config (set_context: false, reads existing) - | - +---> 5. Build MintActionMetaConfig with compressible_cmint - | - +---> 6. invoke/invoke_signed to ctoken program - | - Creates CMint PDA on-chain (DECOMPRESSED/"HOT") - | - Registers mint's compressed address - | - Light System reads PDAs from CPI context - | - All addresses registered atomically - | - v - Return Ok(true) - | - v -Instruction Body - (Can use HOT CMint: mintTo, burn, transfer, etc.) - | - v -light_finalize() -> Ok(()) [no-op] - | - v -Anchor Exit (serializes all account data) -``` - -### 4. Key Design Decisions - -**All compression in pre_init**: - -- CMint is created and decompressed BEFORE instruction body runs -- Instruction body can immediately use the HOT mint (mintTo, burn, etc.) -- This enables patterns like `raydium-cp-swap` where mint operations follow creation - -**with_data=false for PDAs**: - -- Compressed account only gets the address (no data hash) -- Actual data stays on-chain PDA with CompressionInfo -- Later auto-compression will fully compress and close the PDA -- SDK enforces this: `with_data=true` throws "not supported yet" - -**CPI Context Batching**: When PDAs and mints are combined: - -1. PDAs are written to CPI context first via `write_to_cpi_context_first()` -2. Mint action reads from the same CPI context (set_context: false) -3. Light System processes all operations atomically - -**Tree Indexing**: Critical for CPI context validation: - -- `in_tree_index` is 1-indexed (Light System does `in_tree_index - 1`) -- Points to the state queue, which has `associated_merkle_tree` -- Must match the CPI context's `associated_merkle_tree` - -### 5. Required Accounts for Combined Flow - -```rust -pub struct CreatePdasAndMintAuto<'info> { - pub fee_payer: Signer<'info>, - pub authority: Signer<'info>, - pub mint_authority: Signer<'info>, - pub mint_signer: UncheckedAccount<'info>, // CMint derives from this - - #[compressible(...)] - pub user_record: Account<'info, UserRecord>, // PDA to compress - - #[compressible(...)] - pub game_session: Account<'info, GameSession>, // Another PDA - - #[light_mint(...)] - pub lp_mint: UncheckedAccount<'info>, // CMint placeholder (HOT after pre_init) - - pub vault: UncheckedAccount<'info>, // Program-owned CToken vault - pub vault_authority: UncheckedAccount<'info>, // Vault owner PDA - pub user_ata: UncheckedAccount<'info>, // User's ATA for lp_mint - - pub compression_config: AccountInfo<'info>, // Light protocol config - pub ctoken_compressible_config: AccountInfo<'info>, // Ctoken config - pub ctoken_rent_sponsor: AccountInfo<'info>, // Rent sponsor - pub light_token_program: AccountInfo<'info>, // Ctoken program - pub ctoken_cpi_authority: AccountInfo<'info>, // Ctoken CPI authority - pub system_program: Program<'info, System>, -} -``` - -### 6. Instruction Body: Using the HOT CMint - -After `light_pre_init()` creates and decompresses the CMint, the instruction body can immediately use it: - -```rust -#[light_instruction(params)] -pub fn create_pdas_and_mint_auto<'info>(ctx: ..., params: ...) -> Result<()> { - // 1. Populate PDA data (compression handled by macro) - ctx.accounts.user_record.owner = params.owner; - ctx.accounts.game_session.session_id = params.session_id; - - // 2. Create program-owned CToken vault (like cp-swap's token vaults) - CreateTokenAccountCpi { - payer: ctx.accounts.fee_payer.to_account_info(), - account: ctx.accounts.vault.to_account_info(), - mint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint from pre_init - owner: ctx.accounts.vault_authority.key(), - compressible: CompressibleParamsCpi { ... }, - }.invoke_signed(&[vault_seeds])?; - - // 3. Create user's ATA (like cp-swap's creator_lp_token) - CreateAssociatedCTokenAccountCpi { - owner: ctx.accounts.fee_payer.to_account_info(), - mint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint - associated_token_account: ctx.accounts.user_ata.to_account_info(), - compressible: CompressibleParamsCpi { ... }, - }.invoke()?; - - // 4. Mint tokens to vault and user's ATA - CTokenMintToCpi { - cmint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint - destination: ctx.accounts.vault.to_account_info(), - amount: params.vault_mint_amount, - authority: ctx.accounts.mint_authority.to_account_info(), - }.invoke()?; - - CTokenMintToCpi { - cmint: ctx.accounts.lp_mint.to_account_info(), // HOT CMint - destination: ctx.accounts.user_ata.to_account_info(), - amount: params.user_ata_mint_amount, - authority: ctx.accounts.mint_authority.to_account_info(), - }.invoke()?; - - Ok(()) -} -``` - -### 7. Test: `test_create_pdas_and_mint_auto` - -Demonstrates the full cp-swap-like flow: - -1. Setup compression config and signers -2. Derive PDAs, CMint, vault, and user_ata addresses -3. Get validity proof for all 3 compressed addresses (2 PDAs + 1 mint) -4. Build instruction with CPI context enabled -5. Execute single transaction -6. Verify: - - 2 PDAs compressed (address only, data on-chain) - - 1 CMint created and decompressed (HOT) - - 1 Program-owned vault with correct balance (e.g., 100 tokens) - - 1 User ATA with correct balance (e.g., 50 tokens) - - Both vault and ATA owned by ctoken program - -## Conclusion - -The macro system enables atomic creation of an arbitrary combination of compressed PDAs and decompressed mints in a single instruction with a single proof. All compression logic runs in `light_pre_init()`, so the instruction body can immediately use the HOT CMint for operations like `mintTo`, `burn`, and `transfer`. This pattern is essential for programs like `raydium-cp-swap` where multiple accounts (pool state, observation state, LP mint, token vaults, user ATAs) must be created and operated on atomically. - -**The full flow in one instruction:** - -1. `pre_init()`: Compress 2 PDAs + Create+Decompress CMint (atomically) -2. `instruction body`: Create vault + Create user_ata + MintTo both -3. `finalize()`: no-op - -All accounts (PDAs, CMint, vault, user_ata) exist and are usable within the same instruction. 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 d40b4ab65c..f43bb8afd2 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 @@ -9,15 +9,7 @@ use light_program_test::{ Indexer, ProgramTestConfig, Rpc, }; use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; -use light_token_interface::{ - instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, - state::CompressedMintMetadata, -}; -use light_token_sdk::compressed_token::create_compressed_mint::{ - derive_mint_compressed_address, find_mint_address, -}; use light_token_sdk::token::find_mint_address as find_cmint_address; -use light_token_types::CPI_AUTHORITY_PDA; use solana_instruction::Instruction; use solana_keypair::Keypair; use solana_pubkey::Pubkey; @@ -30,8 +22,10 @@ const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcA /// After warp: all cold (auto-compressed) with non-empty compressed data. #[tokio::test] async fn test_create_pdas_and_mint_auto() { - use csdk_anchor_full_derived_test::instruction_accounts::{LP_MINT_SIGNER_SEED, VAULT_SEED}; - use csdk_anchor_full_derived_test::FullAutoWithMintParams; + use csdk_anchor_full_derived_test::{ + instruction_accounts::{LP_MINT_SIGNER_SEED, VAULT_SEED}, + FullAutoWithMintParams, + }; use light_token_interface::state::Token; use light_token_sdk::token::{ get_associated_token_address_and_bump, COMPRESSIBLE_CONFIG_V1, diff --git a/sdk-tests/sdk-light-token-test/src/decompress_cmint.rs b/sdk-tests/sdk-light-token-test/src/decompress_mint.rs similarity index 100% rename from sdk-tests/sdk-light-token-test/src/decompress_cmint.rs rename to sdk-tests/sdk-light-token-test/src/decompress_mint.rs diff --git a/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs b/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs index b421501d37..296b4cd222 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs @@ -48,11 +48,11 @@ async fn test_approve_invoke() { let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // token_account - AccountMeta::new_readonly(delegate.pubkey(), false), // delegate - AccountMeta::new(payer.pubkey(), true), // owner (signer) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(ata, false), // token_account + AccountMeta::new_readonly(delegate.pubkey(), false), // delegate + AccountMeta::new(payer.pubkey(), true), // owner (signer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -107,11 +107,11 @@ async fn test_approve_invoke_signed() { let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // token_account - AccountMeta::new_readonly(delegate.pubkey(), false), // delegate - AccountMeta::new(pda_owner, false), // PDA owner (program signs) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(ata, false), // token_account + AccountMeta::new_readonly(delegate.pubkey(), false), // delegate + AccountMeta::new(pda_owner, false), // PDA owner (program signs) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -198,10 +198,10 @@ async fn test_revoke_invoke() { let revoke_instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // token_account - AccountMeta::new(payer.pubkey(), true), // owner (signer) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(ata, false), // token_account + AccountMeta::new(payer.pubkey(), true), // owner (signer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: revoke_instruction_data, }; @@ -284,10 +284,10 @@ async fn test_revoke_invoke_signed() { let revoke_instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // token_account - AccountMeta::new(pda_owner, false), // PDA owner (program signs) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new(ata, false), // token_account + AccountMeta::new(pda_owner, false), // PDA owner (program signs) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: revoke_instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_burn.rs b/sdk-tests/sdk-light-token-test/tests/test_burn.rs index 4c0df1abac..fecab7c855 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_burn.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_burn.rs @@ -52,9 +52,9 @@ async fn test_burn_invoke() { let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // source - AccountMeta::new(mint_pda, false), // cmint - AccountMeta::new_readonly(payer.pubkey(), true), // authority (signer) + AccountMeta::new(ata, false), // source + AccountMeta::new(mint_pda, false), // cmint + AccountMeta::new_readonly(payer.pubkey(), true), // authority (signer) AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, @@ -118,9 +118,9 @@ async fn test_burn_invoke_signed() { let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // source - AccountMeta::new(mint_pda, false), // cmint - AccountMeta::new_readonly(pda_owner, false), // PDA authority (program signs) + AccountMeta::new(ata, false), // source + AccountMeta::new(mint_pda, false), // cmint + AccountMeta::new_readonly(pda_owner, false), // PDA authority (program signs) AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, diff --git a/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs b/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs index 3ef3816f08..86ae32e091 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs @@ -117,7 +117,7 @@ async fn test_freeze_invoke_signed() { AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(mint_pda, false), // mint AccountMeta::new_readonly(pda_freeze_authority, false), // PDA freeze_authority (program signs) - AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -283,7 +283,7 @@ async fn test_thaw_invoke_signed() { AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(mint_pda, false), // mint AccountMeta::new_readonly(pda_freeze_authority, false), // PDA freeze_authority (program signs) - AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: thaw_instruction_data, }; From ec96f6cd1cd50eb5386586a2c5bb934b4f9df358 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 16 Jan 2026 21:14:49 +0000 Subject: [PATCH 9/9] fix build --- pnpm-lock.yaml | 4 ---- .../transfer2/compression/ctoken/compress_and_close.rs | 4 ++-- sdk-libs/program-test/Cargo.toml | 4 ++-- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01de124b6e..3476b032c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -448,8 +448,6 @@ importers: programs: {} - sdk-tests/csdk-anchor-derived-test: {} - sdk-tests/csdk-anchor-full-derived-test: {} sdk-tests/sdk-anchor-test: @@ -489,8 +487,6 @@ importers: specifier: ^4.3.5 version: 4.9.5 - sdk-tests/sdk-compressible-test: {} - tsconfig: {} packages: diff --git a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs index 733249ba4e..c42c5dface 100644 --- a/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs +++ b/programs/compressed-token/program/src/compressed_token/transfer2/compression/ctoken/compress_and_close.rs @@ -20,7 +20,7 @@ use spl_pod::solana_msg::msg; use super::inputs::CompressAndCloseInputs; #[cfg(target_os = "solana")] -use crate::token::close::accounts::CloseTokenAccountAccounts; +use crate::ctoken::close::accounts::CloseTokenAccountAccounts; use crate::{ compressed_token::transfer2::accounts::Transfer2Accounts, shared::convert_program_error, }; @@ -272,7 +272,7 @@ pub fn close_for_compress_and_close( let authority = validated_accounts .packed_accounts .get_u8(compression.authority, "CompressAndClose: authority")?; - use crate::token::close::processor::close_token_account; + use crate::ctoken::close::processor::close_token_account; close_token_account(&CloseTokenAccountAccounts { token_account: token_account_info, destination, diff --git a/sdk-libs/program-test/Cargo.toml b/sdk-libs/program-test/Cargo.toml index 15324540df..d4c3073d07 100644 --- a/sdk-libs/program-test/Cargo.toml +++ b/sdk-libs/program-test/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [features] default = [] -devenv = ["v2","light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-token-interface", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"] +devenv = ["v2","light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"] v2 = ["light-client/v2"] [dependencies] @@ -18,7 +18,7 @@ light-merkle-tree-reference = { workspace = true } light-merkle-tree-metadata = { workspace = true, features = ["anchor"] } light-concurrent-merkle-tree = { workspace = true, optional = true } light-hasher = { workspace = true, features = ["poseidon", "sha256", "keccak", "std"] } -light-token-interface = { workspace = true, optional = true } +light-token-interface = { workspace = true } light-compressible = { workspace = true, optional = true } light-token-sdk = { workspace = true } light-compressed-account = { workspace = true, features = ["anchor", "poseidon"] }