diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 98d50f2e22..482b9edabc 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -50,7 +50,9 @@ jobs: - program: native sub-tests: '["cargo-test-sbf -p sdk-native-test", "cargo-test-sbf -p sdk-v1-native-test", "cargo-test-sbf -p sdk-light-token-test", "cargo-test-sbf -p client-test"]' - program: anchor & pinocchio - sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-compressible-test", "cargo-test-sbf -p csdk-anchor-derived-test", "cargo-test-sbf -p csdk-anchor-full-derived-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test", "cargo-test-sbf -p single-mint-test", "cargo-test-sbf -p single-pda-test", "cargo-test-sbf -p single-ata-test", "cargo-test-sbf -p single-token-test"]' + sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-compressible-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test", "cargo-test-sbf -p sdk-light-token-pinocchio-test"]' + - program: light-account + sub-tests: '["cargo-test-sbf -p csdk-anchor-derived-test", "cargo-test-sbf -p csdk-anchor-full-derived-test", "cargo-test-sbf -p single-mint-test", "cargo-test-sbf -p single-pda-test", "cargo-test-sbf -p single-ata-test", "cargo-test-sbf -p single-token-test", "cargo-test-sbf -p single-account-loader-test", "cargo-test-sbf -p anchor-manual-test", "cargo-test-sbf -p anchor-semi-manual-test", "cargo-test-sbf -p pinocchio-manual-test", "cargo-test-sbf -p pinocchio-light-program-test"]' - program: token test sub-tests: '["cargo-test-sbf -p sdk-token-test", "cargo-test-sbf -p token-client-test"]' - program: sdk-libs diff --git a/Cargo.lock b/Cargo.lock index 61b91ceadd..7f8303eafb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -395,6 +395,72 @@ dependencies = [ "serde", ] +[[package]] +name = "anchor-manual-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "bytemuck", + "light-account", + "light-client", + "light-compressed-account", + "light-compressible", + "light-hasher", + "light-macros", + "light-program-test", + "light-test-utils", + "light-token", + "light-token-client", + "light-token-interface", + "light-token-types", + "solana-account-info", + "solana-instruction", + "solana-keypair", + "solana-msg 2.2.1", + "solana-program", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signer", + "tokio", +] + +[[package]] +name = "anchor-semi-manual-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "bytemuck", + "light-account", + "light-anchor-spl", + "light-batched-merkle-tree", + "light-client", + "light-compressed-account", + "light-compressible", + "light-hasher", + "light-macros", + "light-program-test", + "light-sdk-types", + "light-test-utils", + "light-token", + "light-token-interface", + "light-token-types", + "rand 0.8.5", + "solana-account", + "solana-account-info", + "solana-instruction", + "solana-keypair", + "solana-msg 2.2.1", + "solana-program", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signer", + "tokio", +] + [[package]] name = "anchor-spl" version = "0.31.1" @@ -1622,6 +1688,7 @@ dependencies = [ "borsh 0.10.4", "bytemuck", "csdk-anchor-full-derived-test-sdk", + "light-account", "light-anchor-spl", "light-batched-merkle-tree", "light-client", @@ -1629,13 +1696,11 @@ dependencies = [ "light-compressed-token-sdk", "light-compressible", "light-hasher", - "light-heap", "light-instruction-decoder", "light-instruction-decoder-derive", "light-macros", "light-program-test", "light-sdk", - "light-sdk-macros", "light-sdk-types", "light-test-utils", "light-token", @@ -1666,6 +1731,7 @@ dependencies = [ "ahash", "anchor-lang", "csdk-anchor-full-derived-test", + "light-account", "light-client", "light-compressed-token-sdk", "light-sdk", @@ -2331,6 +2397,7 @@ dependencies = [ "hex", "itertools 0.14.0", "lazy_static", + "light-account", "light-account-checks", "light-batched-merkle-tree", "light-client", @@ -3460,21 +3527,59 @@ dependencies = [ "libsecp256k1-core", ] +[[package]] +name = "light-account" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "light-account-checks", + "light-compressed-account", + "light-compressible", + "light-hasher", + "light-macros", + "light-sdk-macros", + "light-sdk-types", + "light-token-interface", + "solana-account-info", + "solana-instruction", + "solana-pubkey 2.4.0", +] + [[package]] name = "light-account-checks" version = "0.7.0" dependencies = [ "borsh 0.10.4", "pinocchio", + "pinocchio-system", "rand 0.8.5", "solana-account-info", + "solana-cpi", + "solana-instruction", "solana-msg 2.2.1", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", + "solana-system-interface 1.0.0", "solana-sysvar", "thiserror 2.0.17", ] +[[package]] +name = "light-account-pinocchio" +version = "0.1.0" +dependencies = [ + "light-account-checks", + "light-compressed-account", + "light-hasher", + "light-macros", + "light-sdk-macros", + "light-sdk-types", + "light-token-interface", + "pinocchio", + "solana-instruction", + "solana-pubkey 2.4.0", +] + [[package]] name = "light-anchor-spl" version = "0.31.1" @@ -3562,6 +3667,7 @@ dependencies = [ "bs58", "futures", "lazy_static", + "light-account", "light-compressed-account", "light-compressed-token-sdk", "light-compressible", @@ -3572,6 +3678,7 @@ dependencies = [ "light-merkle-tree-metadata", "light-prover-client", "light-sdk", + "light-sdk-types", "light-token", "light-token-interface", "litesvm", @@ -3677,6 +3784,7 @@ dependencies = [ "anchor-lang", "arrayvec", "borsh 0.10.4", + "light-account", "light-account-checks", "light-compressed-account", "light-program-profiler", @@ -3948,6 +4056,7 @@ dependencies = [ "bs58", "bytemuck", "chrono", + "light-account", "light-account-checks", "light-batched-merkle-tree", "light-client", @@ -4055,7 +4164,6 @@ dependencies = [ "bytemuck", "light-account-checks", "light-compressed-account", - "light-compressible", "light-concurrent-merkle-tree", "light-hasher", "light-heap", @@ -4123,11 +4231,15 @@ version = "0.19.0" dependencies = [ "anchor-lang", "borsh 0.10.4", + "bytemuck", "light-account-checks", "light-compressed-account", + "light-compressible", "light-hasher", "light-macros", + "light-token-interface", "solana-msg 2.2.1", + "solana-program-error 2.2.2", "solana-pubkey 2.4.0", "thiserror 2.0.17", ] @@ -4250,6 +4362,7 @@ dependencies = [ "anchor-lang", "arrayvec", "borsh 0.10.4", + "light-account", "light-account-checks", "light-batched-merkle-tree", "light-compressed-account", @@ -4335,6 +4448,20 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "light-token-pinocchio" +version = "0.4.0" +dependencies = [ + "borsh 0.10.4", + "light-account-checks", + "light-compressed-account", + "light-macros", + "light-sdk-types", + "light-token-interface", + "pinocchio", + "pinocchio-pubkey", +] + [[package]] name = "light-token-types" version = "0.4.0" @@ -4481,40 +4608,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "manual-test" -version = "0.1.0" -dependencies = [ - "anchor-lang", - "borsh 0.10.4", - "bytemuck", - "light-client", - "light-compressed-account", - "light-compressible", - "light-hasher", - "light-heap", - "light-macros", - "light-program-test", - "light-sdk", - "light-sdk-macros", - "light-sdk-types", - "light-test-utils", - "light-token", - "light-token-client", - "light-token-interface", - "light-token-types", - "solana-account-info", - "solana-instruction", - "solana-keypair", - "solana-msg 2.2.1", - "solana-program", - "solana-program-error 2.2.2", - "solana-pubkey 2.4.0", - "solana-sdk", - "solana-signer", - "tokio", -] - [[package]] name = "matchers" version = "0.2.0" @@ -5076,12 +5169,78 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b971851087bc3699b001954ad02389d50c41405ece3548cbcafc88b3e20017a" +[[package]] +name = "pinocchio-light-program-test" +version = "0.1.0" +dependencies = [ + "borsh 0.10.4", + "bytemuck", + "light-account", + "light-account-pinocchio", + "light-batched-merkle-tree", + "light-client", + "light-compressed-account", + "light-compressible", + "light-hasher", + "light-macros", + "light-program-test", + "light-sdk-types", + "light-test-utils", + "light-token", + "light-token-client", + "light-token-interface", + "light-token-types", + "pinocchio", + "pinocchio-pubkey", + "pinocchio-system", + "solana-account", + "solana-instruction", + "solana-keypair", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signer", + "tokio", +] + [[package]] name = "pinocchio-log" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd11022408f312e6179ece321c1f7dc0d1b2aa7765fddd39b2a7378d65a899e8" +[[package]] +name = "pinocchio-manual-test" +version = "0.1.0" +dependencies = [ + "borsh 0.10.4", + "bytemuck", + "light-account-pinocchio", + "light-client", + "light-compressed-account", + "light-compressible", + "light-hasher", + "light-macros", + "light-program-test", + "light-test-utils", + "light-token", + "light-token-client", + "light-token-interface", + "light-token-types", + "pinocchio", + "pinocchio-pubkey", + "pinocchio-system", + "solana-instruction", + "solana-keypair", + "solana-msg 2.2.1", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signer", + "tokio", +] + [[package]] name = "pinocchio-nostd-test" version = "0.1.0" @@ -6098,6 +6257,31 @@ dependencies = [ "tokio", ] +[[package]] +name = "sdk-light-token-pinocchio-test" +version = "0.1.0" +dependencies = [ + "anchor-spl", + "borsh 0.10.4", + "light-client", + "light-compressed-account", + "light-compressible", + "light-macros", + "light-program-test", + "light-sdk", + "light-test-utils", + "light-token", + "light-token-client", + "light-token-interface", + "light-token-pinocchio", + "light-token-types", + "pinocchio", + "solana-sdk", + "spl-pod", + "spl-token-2022 7.0.0", + "tokio", +] + [[package]] name = "sdk-light-token-test" version = "0.1.0" @@ -6536,16 +6720,14 @@ dependencies = [ "anchor-lang", "borsh 0.10.4", "bytemuck", + "light-account", "light-client", "light-compressed-account", "light-compressible", "light-hasher", - "light-heap", "light-macros", "light-program-test", "light-sdk", - "light-sdk-macros", - "light-sdk-types", "light-test-utils", "light-token", "solana-account-info", @@ -6566,20 +6748,17 @@ version = "0.1.0" dependencies = [ "anchor-lang", "borsh 0.10.4", + "light-account", "light-client", "light-compressed-account", - "light-compressible", "light-hasher", - "light-heap", "light-macros", "light-program-test", "light-sdk", - "light-sdk-macros", "light-sdk-types", "light-test-utils", "light-token", "light-token-interface", - "light-token-types", "solana-account-info", "solana-instruction", "solana-keypair", @@ -6598,16 +6777,14 @@ version = "0.1.0" dependencies = [ "anchor-lang", "borsh 0.10.4", + "light-account", "light-anchor-spl", "light-client", "light-compressed-account", - "light-compressible", "light-hasher", - "light-heap", "light-macros", "light-program-test", "light-sdk", - "light-sdk-macros", "light-sdk-types", "light-test-utils", "light-token", @@ -6631,19 +6808,15 @@ version = "0.1.0" dependencies = [ "anchor-lang", "borsh 0.10.4", + "light-account", "light-client", "light-compressed-account", - "light-compressible", "light-hasher", - "light-heap", "light-macros", "light-program-test", "light-sdk", - "light-sdk-macros", - "light-sdk-types", "light-test-utils", "light-token", - "light-token-types", "solana-account-info", "solana-instruction", "solana-keypair", @@ -6662,15 +6835,13 @@ version = "0.1.0" dependencies = [ "anchor-lang", "borsh 0.10.4", + "light-account", "light-client", "light-compressed-account", - "light-compressible", "light-hasher", - "light-heap", "light-macros", "light-program-test", "light-sdk", - "light-sdk-macros", "light-sdk-types", "light-test-utils", "light-token", diff --git a/Cargo.toml b/Cargo.toml index b708d9c74d..f900941691 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,10 +25,13 @@ members = [ "anchor-programs/system", "sdk-libs/client", "sdk-libs/token-sdk", + "sdk-libs/token-pinocchio", "sdk-libs/compressed-token-sdk", "sdk-libs/event", "sdk-libs/token-client", "sdk-libs/macros", + "sdk-libs/account", + "sdk-libs/account-pinocchio", "sdk-libs/sdk", "sdk-libs/sdk-pinocchio", "sdk-libs/sdk-types", @@ -58,15 +61,19 @@ members = [ "sdk-tests/sdk-v1-native-test", "sdk-tests/sdk-token-test", "sdk-tests/sdk-light-token-test", + "sdk-tests/sdk-light-token-pinocchio", "sdk-tests/token-client-test", "sdk-tests/csdk-anchor-full-derived-test", "sdk-tests/csdk-anchor-full-derived-test-sdk", "sdk-tests/single-mint-test", "sdk-tests/single-pda-test", + "sdk-tests/anchor-semi-manual-test", "sdk-tests/single-account-loader-test", "sdk-tests/single-ata-test", "sdk-tests/single-token-test", - "sdk-tests/manual-test", + "sdk-tests/anchor-manual-test", + "sdk-tests/pinocchio-manual-test", + "sdk-tests/pinocchio-light-program-test", "forester-utils", "forester", "sparse-merkle-tree", @@ -198,6 +205,8 @@ light-macros = { path = "program-libs/macros", version = "2.2.0" } light-merkle-tree-reference = { path = "program-tests/merkle-tree", version = "4.0.0" } light-heap = { path = "program-libs/heap", version = "2.0.0" } light-prover-client = { path = "prover/client", version = "6.0.0" } +light-account = { path = "sdk-libs/account", version = "0.1.0", default-features = false } +light-account-pinocchio = { path = "sdk-libs/account-pinocchio", version = "0.1.0", default-features = false } light-sdk = { path = "sdk-libs/sdk", version = "0.19.0" } light-sdk-pinocchio = { path = "sdk-libs/sdk-pinocchio", version = "0.19.0" } light-sdk-macros = { path = "sdk-libs/macros", version = "0.19.0" } @@ -220,6 +229,7 @@ light-compressed-token = { path = "programs/compressed-token/program", version = ] } light-token-types = { path = "sdk-libs/token-types", version = "0.4.0" } light-token = { path = "sdk-libs/token-sdk", version = "0.4.0" } +light-token-pinocchio = { path = "sdk-libs/token-pinocchio", version = "0.4.0" } light-compressed-token-sdk = { path = "sdk-libs/compressed-token-sdk", version = "0.1.0" } light-token-client = { path = "sdk-libs/token-client", version = "0.1.1" } light-system-program-anchor = { path = "anchor-programs/system", version = "2.0.0", features = [ diff --git a/forester/Cargo.toml b/forester/Cargo.toml index 5b23dc2855..3b0cc6aef2 100644 --- a/forester/Cargo.toml +++ b/forester/Cargo.toml @@ -26,6 +26,7 @@ forester-utils = { workspace = true } light-client = { workspace = true, features = ["v2"] } light-merkle-tree-metadata = { workspace = true } light-sdk = { workspace = true, features = ["anchor"] } +light-account = { workspace = true } light-program-test = { workspace = true } light-compressible = { workspace = true, default-features = false, features = ["solana"] } light-token-interface = { workspace = true } diff --git a/forester/src/compressible/pda/compressor.rs b/forester/src/compressible/pda/compressor.rs index 188057544a..45c64c7fae 100644 --- a/forester/src/compressible/pda/compressor.rs +++ b/forester/src/compressible/pda/compressor.rs @@ -6,6 +6,7 @@ use std::sync::{ use borsh::BorshDeserialize; use forester_utils::rpc_pool::SolanaRpcPool; use futures::StreamExt; +use light_account::LightConfig; use light_account_checks::discriminator::DISCRIMINATOR_LEN; use light_client::{ indexer::Indexer, @@ -15,7 +16,6 @@ use light_client::{ rpc::Rpc, }; use light_compressed_account::address::derive_address; -use light_sdk::interface::config::LightConfig; use solana_sdk::{ instruction::AccountMeta, pubkey::Pubkey, @@ -80,7 +80,10 @@ impl PdaCompressor { let program_id = &program_config.program_id; // Get the compressible config PDA for this program (config_bump = 0) - let (config_pda, _) = LightConfig::derive_pda(program_id, 0); + let (config_pda, _) = Pubkey::find_program_address( + &[light_account::LIGHT_CONFIG_SEED, &0u16.to_le_bytes()], + program_id, + ); // Fetch the config to get rent_sponsor and address_space let rpc = self.rpc_pool.get_connection().await?; @@ -105,12 +108,13 @@ impl PdaCompressor { ) })?; - let rent_sponsor = config.rent_sponsor; - let compression_authority = config.compression_authority; - let address_tree = *config + let rent_sponsor: Pubkey = config.rent_sponsor.into(); + let compression_authority: Pubkey = config.compression_authority.into(); + let address_tree: Pubkey = (*config .address_space .first() - .ok_or_else(|| anyhow::anyhow!("Config has no address space"))?; + .ok_or_else(|| anyhow::anyhow!("Config has no address space"))?) + .into(); // CompressAccountsIdempotent expects 4 accounts: // 1. fee_payer (signer, writable) diff --git a/forester/src/compressible/pda/state.rs b/forester/src/compressible/pda/state.rs index 6f96bb4f20..af00bc806e 100644 --- a/forester/src/compressible/pda/state.rs +++ b/forester/src/compressible/pda/state.rs @@ -1,9 +1,9 @@ use borsh::BorshDeserialize; use dashmap::DashMap; +use light_account::CompressionInfo; use light_compressible::rent::{ get_last_funded_epoch, get_rent_exemption_lamports, SLOTS_PER_EPOCH, }; -use light_sdk::compressible::compression_info::CompressionInfo; use solana_sdk::pubkey::Pubkey; use tracing::{debug, warn}; diff --git a/program-libs/account-checks/Cargo.toml b/program-libs/account-checks/Cargo.toml index 89290c8671..9dd8fd7653 100644 --- a/program-libs/account-checks/Cargo.toml +++ b/program-libs/account-checks/Cargo.toml @@ -15,11 +15,14 @@ solana = [ "solana-sysvar", "solana-account-info", "solana-pubkey", + "solana-cpi", + "solana-system-interface", + "solana-instruction", "msg", "std" ] msg = ["dep:solana-msg"] -pinocchio = ["dep:pinocchio"] +pinocchio = ["dep:pinocchio", "dep:pinocchio-system"] test-only = ["dep:rand", "std"] [dependencies] @@ -31,7 +34,11 @@ solana-pubkey = { workspace = true, optional = true, features = [ "sha2", ] } solana-msg = { workspace = true, optional = true } +solana-cpi = { workspace = true, optional = true } +solana-instruction = { workspace = true, optional = true } +solana-system-interface = { workspace = true, optional = true, features = ["bincode"] } pinocchio = { workspace = true, optional = true } +pinocchio-system = { workspace = true, optional = true } thiserror = { workspace = true } rand = { workspace = true, optional = true } diff --git a/program-libs/account-checks/src/account_info/account_info_trait.rs b/program-libs/account-checks/src/account_info/account_info_trait.rs index 14eddd2b26..12ce7914ea 100644 --- a/program-libs/account-checks/src/account_info/account_info_trait.rs +++ b/program-libs/account-checks/src/account_info/account_info_trait.rs @@ -5,6 +5,17 @@ use core::{ use crate::error::AccountError; +/// Lightweight owned account metadata for CPI instruction building. +/// +/// Replaces solana_instruction::AccountMeta / pinocchio::instruction::AccountMeta +/// as a framework-agnostic type that can be stored in collections without lifetime issues. +#[derive(Clone, Debug)] +pub struct CpiMeta { + pub pubkey: [u8; 32], + pub is_signer: bool, + pub is_writable: bool, +} + /// Trait to abstract over different AccountInfo implementations (pinocchio vs solana) pub trait AccountInfoTrait { type Pubkey: Copy + Clone + Debug + PartialEq; @@ -17,6 +28,9 @@ pub trait AccountInfoTrait { /// Return raw byte array for maximum compatibility fn key(&self) -> [u8; 32]; + /// Return a reference to the key bytes with the lifetime of the account. + /// Used when building seed ref arrays that borrow from the accounts slice. + fn key_ref(&self) -> &[u8]; /// Return the pubkey in the native format fn pubkey(&self) -> Self::Pubkey; fn is_writable(&self) -> bool; @@ -45,7 +59,89 @@ pub trait AccountInfoTrait { /// Get minimum rent balance for a given size fn get_min_rent_balance(size: usize) -> Result; + /// Get the current clock slot from sysvar. + /// Only meaningful on-chain; implementations may error off-chain. + fn get_current_slot() -> Result; + + /// Assign the account to a new owner program. + fn assign(&self, new_owner: &[u8; 32]) -> Result<(), AccountError>; + + /// Resize the account data (truncating or zero-extending). + fn realloc(&self, new_len: usize, zero_init: bool) -> Result<(), AccountError>; + + /// Subtract lamports from the account (checked). + fn sub_lamports(&self, amount: u64) -> Result<(), AccountError>; + + /// Add lamports to the account (checked). + fn add_lamports(&self, amount: u64) -> Result<(), AccountError>; + fn data_is_empty(&self) -> bool { self.data_len() == 0 } + + /// Close this account: zero data, transfer all lamports to destination, + /// assign to system program. + fn close(&self, destination: &Self) -> Result<(), AccountError>; + + /// Create a PDA account via system program CPI (invoke_signed). + /// + /// `self` is the uninitialized PDA account to be created. + /// Handles the edge case where the account already has lamports + /// (e.g. attacker donation) by falling back to Assign + Allocate + Transfer. + /// + /// # Arguments + /// * `lamports` - Amount of lamports for rent-exemption + /// * `space` - Size of the account data in bytes + /// * `owner` - Program that will own the created account + /// * `pda_seeds` - Seeds for this PDA (including bump) for signing + /// * `rent_payer` - Account paying for rent + /// * `rent_payer_seeds` - Seeds for the rent payer PDA for signing + /// * `system_program` - The system program account + #[allow(clippy::too_many_arguments)] + fn create_pda_account( + &self, + lamports: u64, + space: u64, + owner: &[u8; 32], + pda_seeds: &[&[u8]], + rent_payer: &Self, + rent_payer_seeds: &[&[u8]], + system_program: &Self, + ) -> Result<(), AccountError>; + + /// Transfer lamports by direct lamport manipulation (no CPI). + fn transfer_lamports(&self, destination: &Self, lamports: u64) -> Result<(), AccountError> { + self.sub_lamports(lamports)?; + destination.add_lamports(lamports) + } + + /// Transfer lamports via system program CPI with invoke_signed. + /// Pass `&[]` for `signer_seeds` if the sender is already a signer. + fn transfer_lamports_cpi( + &self, + destination: &Self, + lamports: u64, + signer_seeds: &[&[u8]], + ) -> Result<(), AccountError>; + + /// Invoke an arbitrary program via CPI with optional PDA signing. + /// + /// This is the generic CPI entry point. It builds a native instruction + /// from the decomposed components and calls the runtime's invoke_signed. + /// + /// # Arguments + /// * `program_id` - Target program to invoke + /// * `instruction_data` - Serialized instruction data + /// * `account_metas` - Account metadata describing each account's role + /// * `account_infos` - The actual account info objects (must match metas) + /// * `signer_seeds` - PDA signer seeds; pass `&[]` for no PDA signing + fn invoke_cpi( + program_id: &[u8; 32], + instruction_data: &[u8], + account_metas: &[CpiMeta], + account_infos: &[Self], + signer_seeds: &[&[&[u8]]], + ) -> Result<(), AccountError> + where + Self: Sized; } diff --git a/program-libs/account-checks/src/account_info/account_meta_trait.rs b/program-libs/account-checks/src/account_info/account_meta_trait.rs new file mode 100644 index 0000000000..c46d0aebab --- /dev/null +++ b/program-libs/account-checks/src/account_info/account_meta_trait.rs @@ -0,0 +1,21 @@ +use core::fmt::Debug; + +/// Trait abstracting over AccountMeta implementations (solana vs pinocchio). +/// +/// The associated `Pubkey` type allows callers to pass native pubkey types +/// (e.g. `solana_pubkey::Pubkey` or `[u8; 32]`) without manual conversion. +pub trait AccountMetaTrait: Clone + Debug { + /// The native pubkey type for this account meta implementation. + /// - `solana_pubkey::Pubkey` for `solana_instruction::AccountMeta` + /// - `[u8; 32]` for pinocchio's `OwnedAccountMeta` + type Pubkey: Copy; + + fn new(pubkey: Self::Pubkey, is_signer: bool, is_writable: bool) -> Self; + fn pubkey_to_bytes(pubkey: Self::Pubkey) -> [u8; 32]; + fn pubkey_from_bytes(bytes: [u8; 32]) -> Self::Pubkey; + fn pubkey_bytes(&self) -> [u8; 32]; + fn is_signer(&self) -> bool; + fn is_writable(&self) -> bool; + fn set_is_signer(&mut self, val: bool); + fn set_is_writable(&mut self, val: bool); +} diff --git a/program-libs/account-checks/src/account_info/mod.rs b/program-libs/account-checks/src/account_info/mod.rs index d86788da66..c14ee41695 100644 --- a/program-libs/account-checks/src/account_info/mod.rs +++ b/program-libs/account-checks/src/account_info/mod.rs @@ -1,4 +1,5 @@ pub mod account_info_trait; +pub mod account_meta_trait; #[cfg(feature = "pinocchio")] pub mod pinocchio; #[cfg(feature = "solana")] diff --git a/program-libs/account-checks/src/account_info/pinocchio.rs b/program-libs/account-checks/src/account_info/pinocchio.rs index b6b7c83134..a7794241dc 100644 --- a/program-libs/account-checks/src/account_info/pinocchio.rs +++ b/program-libs/account-checks/src/account_info/pinocchio.rs @@ -1,6 +1,57 @@ -use super::account_info_trait::AccountInfoTrait; +use super::{account_info_trait::AccountInfoTrait, account_meta_trait::AccountMetaTrait}; use crate::error::AccountError; +/// Owned account meta for pinocchio. +/// +/// Pinocchio's native `AccountMeta<'a>` borrows the pubkey, so we need an +/// owned wrapper that can be stored in collections. +#[derive(Clone, Debug)] +pub struct OwnedAccountMeta { + pub pubkey: [u8; 32], + pub is_signer: bool, + pub is_writable: bool, +} + +impl AccountMetaTrait for OwnedAccountMeta { + type Pubkey = [u8; 32]; + + fn new(pubkey: [u8; 32], is_signer: bool, is_writable: bool) -> Self { + Self { + pubkey, + is_signer, + is_writable, + } + } + + fn pubkey_to_bytes(pubkey: [u8; 32]) -> [u8; 32] { + pubkey + } + + fn pubkey_from_bytes(bytes: [u8; 32]) -> [u8; 32] { + bytes + } + + fn pubkey_bytes(&self) -> [u8; 32] { + self.pubkey + } + + fn is_signer(&self) -> bool { + self.is_signer + } + + fn is_writable(&self) -> bool { + self.is_writable + } + + fn set_is_signer(&mut self, val: bool) { + self.is_signer = val; + } + + fn set_is_writable(&mut self, val: bool) { + self.is_writable = val; + } +} + /// Implement trait for pinocchio AccountInfo impl AccountInfoTrait for pinocchio::account_info::AccountInfo { type Pubkey = [u8; 32]; @@ -11,6 +62,10 @@ impl AccountInfoTrait for pinocchio::account_info::AccountInfo { *self.key() } + fn key_ref(&self) -> &[u8] { + self.key() + } + fn pubkey(&self) -> Self::Pubkey { *self.key() } @@ -120,4 +175,229 @@ impl AccountInfoTrait for pinocchio::account_info::AccountInfo { Err(AccountError::FailedBorrowRentSysvar) } } + + fn get_current_slot() -> Result { + #[cfg(target_os = "solana")] + { + use pinocchio::sysvars::Sysvar; + pinocchio::sysvars::clock::Clock::get() + .map(|c| c.slot) + .map_err(|_| AccountError::FailedSysvarAccess) + } + #[cfg(all(not(target_os = "solana"), feature = "solana"))] + { + use solana_sysvar::Sysvar; + solana_sysvar::clock::Clock::get() + .map(|c| c.slot) + .map_err(|_| AccountError::FailedSysvarAccess) + } + #[cfg(all(not(target_os = "solana"), not(feature = "solana")))] + { + Err(AccountError::FailedSysvarAccess) + } + } + + fn assign(&self, new_owner: &[u8; 32]) -> Result<(), AccountError> { + // SAFETY: We trust the caller to provide a valid owner. + // This is safe in the Solana runtime context where the runtime + // validates ownership changes. + unsafe { + self.assign(&pinocchio::pubkey::Pubkey::from(*new_owner)); + } + Ok(()) + } + + fn realloc(&self, new_len: usize, _zero_init: bool) -> Result<(), AccountError> { + self.resize(new_len).map_err(AccountError::from) + } + + fn sub_lamports(&self, amount: u64) -> Result<(), AccountError> { + let mut lamports = self.try_borrow_mut_lamports().map_err(AccountError::from)?; + *lamports = lamports + .checked_sub(amount) + .ok_or(AccountError::ArithmeticOverflow)?; + Ok(()) + } + + fn add_lamports(&self, amount: u64) -> Result<(), AccountError> { + let mut lamports = self.try_borrow_mut_lamports().map_err(AccountError::from)?; + *lamports = lamports + .checked_add(amount) + .ok_or(AccountError::ArithmeticOverflow)?; + Ok(()) + } + + fn close(&self, destination: &Self) -> Result<(), AccountError> { + crate::close_account::close_account(self, destination) + } + + #[inline(never)] + fn create_pda_account( + &self, + lamports: u64, + space: u64, + owner: &[u8; 32], + pda_seeds: &[&[u8]], + rent_payer: &Self, + rent_payer_seeds: &[&[u8]], + _system_program: &Self, + ) -> Result<(), AccountError> { + extern crate alloc; + use alloc::vec::Vec; + + use pinocchio::instruction::{Seed, Signer}; + + let pda_seeds_vec: Vec = pda_seeds.iter().map(|s| Seed::from(*s)).collect(); + let pda_signer = Signer::from(&pda_seeds_vec[..]); + + // Only build payer signer when rent_payer is itself a PDA. + // Passing empty seeds to invoke_signed causes create_program_address(&[], program_id) + // which can fail if the result happens to land on the ed25519 curve. + let payer_seeds_vec: Vec = rent_payer_seeds.iter().map(|s| Seed::from(*s)).collect(); + let has_payer_seeds = !rent_payer_seeds.is_empty(); + + // Cold path: account already has lamports (e.g., attacker donation). + // CreateAccount would fail, so use Assign + Allocate + Transfer. + if self.lamports() > 0 { + pinocchio_system::instructions::Assign { + account: self, + owner, + } + .invoke_signed(core::slice::from_ref(&pda_signer)) + .map_err(AccountError::from)?; + + pinocchio_system::instructions::Allocate { + account: self, + space, + } + .invoke_signed(&[pda_signer]) + .map_err(AccountError::from)?; + + let current_lamports = self.lamports(); + if lamports > current_lamports { + if has_payer_seeds { + let payer_signer = Signer::from(&payer_seeds_vec[..]); + pinocchio_system::instructions::Transfer { + from: rent_payer, + to: self, + lamports: lamports - current_lamports, + } + .invoke_signed(&[payer_signer]) + .map_err(AccountError::from)?; + } else { + pinocchio_system::instructions::Transfer { + from: rent_payer, + to: self, + lamports: lamports - current_lamports, + } + .invoke_signed(&[]) + .map_err(AccountError::from)?; + } + } + + return Ok(()); + } + + // Normal path: CreateAccount + let create_account = pinocchio_system::instructions::CreateAccount { + from: rent_payer, + to: self, + lamports, + space, + owner, + }; + if has_payer_seeds { + let payer_signer = Signer::from(&payer_seeds_vec[..]); + create_account + .invoke_signed(&[payer_signer, pda_signer]) + .map_err(AccountError::from) + } else { + create_account + .invoke_signed(&[pda_signer]) + .map_err(AccountError::from) + } + } + + fn transfer_lamports_cpi( + &self, + destination: &Self, + lamports: u64, + signer_seeds: &[&[u8]], + ) -> Result<(), AccountError> { + extern crate alloc; + use alloc::vec::Vec; + + use pinocchio::instruction::{Seed, Signer}; + + let seeds_vec: Vec = signer_seeds.iter().map(|s| Seed::from(*s)).collect(); + let signer = Signer::from(&seeds_vec[..]); + + pinocchio_system::instructions::Transfer { + from: self, + to: destination, + lamports, + } + .invoke_signed(&[signer]) + .map_err(AccountError::from) + } + + fn invoke_cpi( + program_id: &[u8; 32], + instruction_data: &[u8], + account_metas: &[super::account_info_trait::CpiMeta], + account_infos: &[Self], + signer_seeds: &[&[&[u8]]], + ) -> Result<(), AccountError> { + extern crate alloc; + use alloc::vec::Vec; + + use pinocchio::instruction::{AccountMeta, Seed, Signer}; + + // Build owned pubkeys so AccountMeta can borrow them + let pubkeys: Vec = account_metas + .iter() + .map(|m| pinocchio::pubkey::Pubkey::from(m.pubkey)) + .collect(); + + // Build pinocchio AccountMetas referencing the owned pubkeys + let metas: Vec> = account_metas + .iter() + .zip(pubkeys.iter()) + .map(|(m, pk)| AccountMeta::new(pk, m.is_writable, m.is_signer)) + .collect(); + + let program_pubkey = pinocchio::pubkey::Pubkey::from(*program_id); + let instruction = pinocchio::instruction::Instruction { + program_id: &program_pubkey, + accounts: &metas, + data: instruction_data, + }; + + // Build info_refs by looking up each account_meta's pubkey in account_infos. + // This matches how solana-program's invoke works (lookup by pubkey, not position). + // Pinocchio's invoke_signed_with_bounds zips account_infos with account_metas + // and requires pubkeys to match at each position, so we must reorder. + let mut info_refs: Vec<&pinocchio::account_info::AccountInfo> = + Vec::with_capacity(account_metas.len()); + for meta in account_metas { + let account_info = account_infos + .iter() + .find(|info| info.key() == &meta.pubkey) + .ok_or(AccountError::NotEnoughAccountKeys)?; + info_refs.push(account_info); + } + + // Build signers from seeds + let signer_seed_vecs: Vec> = signer_seeds + .iter() + .map(|seeds| seeds.iter().map(|s| Seed::from(*s)).collect()) + .collect(); + let signers: Vec = signer_seed_vecs + .iter() + .map(|seeds| Signer::from(&seeds[..])) + .collect(); + + pinocchio::cpi::invoke_signed_with_bounds::<64>(&instruction, &info_refs, &signers) + .map_err(AccountError::from) + } } diff --git a/program-libs/account-checks/src/account_info/solana.rs b/program-libs/account-checks/src/account_info/solana.rs index ebeca75dc5..13ebae8e20 100644 --- a/program-libs/account-checks/src/account_info/solana.rs +++ b/program-libs/account-checks/src/account_info/solana.rs @@ -1,4 +1,4 @@ -use super::account_info_trait::AccountInfoTrait; +use super::{account_info_trait::AccountInfoTrait, account_meta_trait::AccountMetaTrait}; use crate::error::AccountError; /// Implement trait for solana AccountInfo @@ -17,6 +17,10 @@ impl AccountInfoTrait for solana_account_info::AccountInfo<'_> { self.key.to_bytes() } + fn key_ref(&self) -> &[u8] { + self.key.as_ref() + } + fn pubkey(&self) -> Self::Pubkey { *self.key } @@ -85,4 +89,238 @@ impl AccountInfoTrait for solana_account_info::AccountInfo<'_> { .map(|rent| rent.minimum_balance(size)) .map_err(|_| AccountError::FailedBorrowRentSysvar) } + + fn get_current_slot() -> Result { + use solana_sysvar::Sysvar; + solana_sysvar::clock::Clock::get() + .map(|c| c.slot) + .map_err(|_| AccountError::FailedSysvarAccess) + } + + fn assign(&self, new_owner: &[u8; 32]) -> Result<(), AccountError> { + self.assign(&solana_pubkey::Pubkey::from(*new_owner)); + Ok(()) + } + + fn realloc(&self, new_len: usize, zero_init: bool) -> Result<(), AccountError> { + #[allow(deprecated)] + self.realloc(new_len, zero_init) + .map_err(|_| AccountError::InvalidAccountSize) + } + + fn sub_lamports(&self, amount: u64) -> Result<(), AccountError> { + let mut lamports = self + .try_borrow_mut_lamports() + .map_err(|_| AccountError::BorrowAccountDataFailed)?; + **lamports = lamports + .checked_sub(amount) + .ok_or(AccountError::ArithmeticOverflow)?; + Ok(()) + } + + fn add_lamports(&self, amount: u64) -> Result<(), AccountError> { + let mut lamports = self + .try_borrow_mut_lamports() + .map_err(|_| AccountError::BorrowAccountDataFailed)?; + **lamports = lamports + .checked_add(amount) + .ok_or(AccountError::ArithmeticOverflow)?; + Ok(()) + } + + fn close(&self, destination: &Self) -> Result<(), AccountError> { + crate::close_account::close_account(self, destination) + } + + #[inline(never)] + fn create_pda_account( + &self, + lamports: u64, + space: u64, + owner: &[u8; 32], + pda_seeds: &[&[u8]], + rent_payer: &Self, + rent_payer_seeds: &[&[u8]], + system_program: &Self, + ) -> Result<(), AccountError> { + use solana_cpi::invoke_signed; + use solana_system_interface::instruction as system_instruction; + + let owner_pubkey = solana_pubkey::Pubkey::from(*owner); + + // Cold path: account already has lamports (e.g., attacker donation). + // CreateAccount would fail, so use Assign + Allocate + Transfer. + if self.lamports() > 0 { + return create_pda_with_lamports_solana( + self, + lamports, + space, + &owner_pubkey, + pda_seeds, + rent_payer, + rent_payer_seeds, + system_program, + ); + } + + // Normal path: CreateAccount + let create_ix = system_instruction::create_account( + rent_payer.key, + self.key, + lamports, + space, + &owner_pubkey, + ); + // Only include rent_payer_seeds when the payer is itself a PDA. + // Passing empty seeds to invoke_signed causes create_program_address(&[], program_id) + // which can fail if the result happens to land on the ed25519 curve. + if rent_payer_seeds.is_empty() { + invoke_signed( + &create_ix, + &[rent_payer.clone(), self.clone()], + &[pda_seeds], + ) + } else { + invoke_signed( + &create_ix, + &[rent_payer.clone(), self.clone()], + &[rent_payer_seeds, pda_seeds], + ) + } + .map_err(|_| AccountError::InvalidAccount) + } + + fn transfer_lamports_cpi( + &self, + destination: &Self, + lamports: u64, + signer_seeds: &[&[u8]], + ) -> Result<(), AccountError> { + use solana_cpi::invoke_signed; + use solana_system_interface::instruction as system_instruction; + + let ix = system_instruction::transfer(self.key, destination.key, lamports); + invoke_signed(&ix, &[self.clone(), destination.clone()], &[signer_seeds]) + .map_err(|_| AccountError::InvalidAccount) + } + + fn invoke_cpi( + program_id: &[u8; 32], + instruction_data: &[u8], + account_metas: &[super::account_info_trait::CpiMeta], + account_infos: &[Self], + signer_seeds: &[&[&[u8]]], + ) -> Result<(), AccountError> { + use solana_cpi::invoke_signed; + + let metas: Vec = account_metas + .iter() + .map(|m| solana_instruction::AccountMeta { + pubkey: solana_pubkey::Pubkey::from(m.pubkey), + is_signer: m.is_signer, + is_writable: m.is_writable, + }) + .collect(); + + let ix = solana_instruction::Instruction { + program_id: solana_pubkey::Pubkey::from(*program_id), + accounts: metas, + data: instruction_data.to_vec(), + }; + + invoke_signed(&ix, account_infos, signer_seeds).map_err(|_| AccountError::InvalidAccount) + } +} + +impl AccountMetaTrait for solana_instruction::AccountMeta { + type Pubkey = solana_pubkey::Pubkey; + + fn new(pubkey: solana_pubkey::Pubkey, is_signer: bool, is_writable: bool) -> Self { + Self { + pubkey, + is_signer, + is_writable, + } + } + + fn pubkey_to_bytes(pubkey: solana_pubkey::Pubkey) -> [u8; 32] { + pubkey.to_bytes() + } + + fn pubkey_from_bytes(bytes: [u8; 32]) -> solana_pubkey::Pubkey { + solana_pubkey::Pubkey::from(bytes) + } + + fn pubkey_bytes(&self) -> [u8; 32] { + self.pubkey.to_bytes() + } + + fn is_signer(&self) -> bool { + self.is_signer + } + + fn is_writable(&self) -> bool { + self.is_writable + } + + fn set_is_signer(&mut self, val: bool) { + self.is_signer = val; + } + + fn set_is_writable(&mut self, val: bool) { + self.is_writable = val; + } +} + +/// Cold path for create_pda_account when account already has lamports. +#[cold] +#[inline(never)] +#[allow(clippy::too_many_arguments)] +fn create_pda_with_lamports_solana<'a>( + account: &solana_account_info::AccountInfo<'a>, + lamports: u64, + space: u64, + owner: &solana_pubkey::Pubkey, + pda_seeds: &[&[u8]], + rent_payer: &solana_account_info::AccountInfo<'a>, + rent_payer_seeds: &[&[u8]], + system_program: &solana_account_info::AccountInfo<'a>, +) -> Result<(), AccountError> { + use solana_cpi::invoke_signed; + use solana_system_interface::instruction as system_instruction; + + let current_lamports = account.lamports(); + + // Assign owner + let assign_ix = system_instruction::assign(account.key, owner); + invoke_signed(&assign_ix, std::slice::from_ref(account), &[pda_seeds]) + .map_err(|_| AccountError::InvalidAccount)?; + + // Allocate space + let allocate_ix = system_instruction::allocate(account.key, space); + invoke_signed(&allocate_ix, std::slice::from_ref(account), &[pda_seeds]) + .map_err(|_| AccountError::InvalidAccount)?; + + // Transfer remaining lamports for rent-exemption if needed + if lamports > current_lamports { + let transfer_ix = + system_instruction::transfer(rent_payer.key, account.key, lamports - current_lamports); + // Only include rent_payer_seeds when the payer is itself a PDA. + if rent_payer_seeds.is_empty() { + invoke_signed( + &transfer_ix, + &[rent_payer.clone(), account.clone(), system_program.clone()], + &[], + ) + } else { + invoke_signed( + &transfer_ix, + &[rent_payer.clone(), account.clone(), system_program.clone()], + &[rent_payer_seeds], + ) + } + .map_err(|_| AccountError::InvalidAccount)?; + } + + Ok(()) } diff --git a/program-libs/account-checks/src/close_account.rs b/program-libs/account-checks/src/close_account.rs new file mode 100644 index 0000000000..d3f9ab85f9 --- /dev/null +++ b/program-libs/account-checks/src/close_account.rs @@ -0,0 +1,31 @@ +use crate::{account_info::account_info_trait::AccountInfoTrait, error::AccountError}; + +/// Close a native Solana account by transferring lamports and clearing data. +/// +/// Transfers all lamports to `sol_destination`, assigns the account to the +/// system program (all-zero owner), and resizes data to 0. +/// +/// If `info` and `sol_destination` are the same account, the lamports stay +/// in the account but the owner and data are cleared. +pub fn close_account( + info: &AI, + sol_destination: &AI, +) -> Result<(), AccountError> { + let system_program_id = [0u8; 32]; + + if info.key() == sol_destination.key() { + info.assign(&system_program_id)?; + info.realloc(0, false)?; + return Ok(()); + } + + let lamports_to_transfer = info.lamports(); + + sol_destination.add_lamports(lamports_to_transfer)?; + info.sub_lamports(lamports_to_transfer)?; + + info.assign(&system_program_id)?; + info.realloc(0, false)?; + + Ok(()) +} diff --git a/program-libs/account-checks/src/error.rs b/program-libs/account-checks/src/error.rs index 1a0d3f0cc3..42bbee32ac 100644 --- a/program-libs/account-checks/src/error.rs +++ b/program-libs/account-checks/src/error.rs @@ -34,8 +34,12 @@ pub enum AccountError { NotEnoughAccountKeys, #[error("Invalid Account.")] InvalidAccount, + #[error("Failed to access sysvar.")] + FailedSysvarAccess, #[error("Pinocchio program error with code: {0}")] PinocchioProgramError(u32), + #[error("Arithmetic overflow.")] + ArithmeticOverflow, } impl From for u32 { @@ -57,7 +61,9 @@ impl From for u32 { AccountError::AccountNotZeroed => 20013, AccountError::NotEnoughAccountKeys => 20014, AccountError::InvalidAccount => 20015, + AccountError::FailedSysvarAccess => 20016, AccountError::PinocchioProgramError(code) => code, + AccountError::ArithmeticOverflow => 20017, } } } diff --git a/program-libs/account-checks/src/lib.rs b/program-libs/account-checks/src/lib.rs index 5d973032b3..8bbd085b3c 100644 --- a/program-libs/account-checks/src/lib.rs +++ b/program-libs/account-checks/src/lib.rs @@ -16,10 +16,15 @@ pub mod account_info; pub mod account_iterator; pub mod checks; +pub mod close_account; pub mod discriminator; pub mod error; pub mod packed_accounts; -pub use account_info::account_info_trait::AccountInfoTrait; +pub use account_info::{ + account_info_trait::{AccountInfoTrait, CpiMeta}, + account_meta_trait::AccountMetaTrait, +}; pub use account_iterator::AccountIterator; +pub use close_account::close_account; pub use error::AccountError; diff --git a/program-libs/compressible/README.md b/program-libs/compressible/README.md index 9b9a6a7a80..573da139cc 100644 --- a/program-libs/compressible/README.md +++ b/program-libs/compressible/README.md @@ -14,7 +14,6 @@ cold account into hot state in-flight when using the account again. |------|-------------| | [`CompressionInfo`](compression_info::CompressionInfo) | Rent state, authorities, and compression config per account | | [`CompressibleConfig`](config::CompressibleConfig) | Program-level config: rent sponsor, authorities, address space | -| [`CreateAccountsProof`] | Validity proof and tree info for account init | | [`RentConfig`](rent::RentConfig) | Rent function parameters for compression eligibility | | [`compression_info`] | `is_compressible`, `claim`, and top-up logic | | [`registry_instructions`] | Instructions for the compression registry | diff --git a/program-libs/compressible/src/config.rs b/program-libs/compressible/src/config.rs index f304fd7c7f..2f406d3ddb 100644 --- a/program-libs/compressible/src/config.rs +++ b/program-libs/compressible/src/config.rs @@ -4,7 +4,7 @@ use solana_pubkey::{pubkey, Pubkey}; use crate::{error::CompressibleError, rent::RentConfig, AnchorDeserialize, AnchorSerialize}; -pub const COMPRESSIBLE_CONFIG_SEED: &[u8] = b"compressible_config"; +pub const LIGHT_CONFIG_SEED: &[u8] = b"compressible_config"; #[derive(Debug, PartialEq)] #[repr(u8)] @@ -238,10 +238,7 @@ impl CompressibleConfig { /// Derives the config PDA address with config bump pub fn derive_pda(program_id: &Pubkey, config_bump: u16) -> (Pubkey, u8) { - Pubkey::find_program_address( - &[COMPRESSIBLE_CONFIG_SEED, &config_bump.to_le_bytes()], - program_id, - ) + Pubkey::find_program_address(&[LIGHT_CONFIG_SEED, &config_bump.to_le_bytes()], program_id) } /// Derives the default config PDA address (config_bump = 1) diff --git a/program-libs/compressible/src/lib.rs b/program-libs/compressible/src/lib.rs index 4973a69d8b..2f0a0e217e 100644 --- a/program-libs/compressible/src/lib.rs +++ b/program-libs/compressible/src/lib.rs @@ -13,7 +13,6 @@ //! |------|-------------| //! | [`CompressionInfo`](compression_info::CompressionInfo) | Rent state, authorities, and compression config per account | //! | [`CompressibleConfig`](config::CompressibleConfig) | Program-level config: rent sponsor, authorities, address space | -//! | [`CreateAccountsProof`] | Validity proof and tree info for account init | //! | [`RentConfig`](rent::RentConfig) | Rent function parameters for compression eligibility | //! | [`compression_info`] | `is_compressible`, `claim`, and top-up logic | //! | [`registry_instructions`] | Instructions for the compression registry | @@ -31,27 +30,6 @@ pub mod rent; pub const DECOMPRESSED_PDA_DISCRIMINATOR: [u8; 8] = [255, 255, 255, 255, 255, 255, 255, 0]; #[cfg(feature = "anchor")] -use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +pub(crate) 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, data::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, - /// State merkle tree index (needed for mint creation decompress validation). - /// This is optional to maintain backwards compatibility. - pub state_tree_index: Option, - /// Offset in remaining_accounts where Light system accounts start. - pub system_accounts_offset: u8, -} +pub(crate) use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; diff --git a/programs/registry/src/compressible/create_config.rs b/programs/registry/src/compressible/create_config.rs index 2da160c9c9..581efa69e8 100644 --- a/programs/registry/src/compressible/create_config.rs +++ b/programs/registry/src/compressible/create_config.rs @@ -1,5 +1,5 @@ use anchor_lang::prelude::*; -use light_compressible::config::{CompressibleConfig, COMPRESSIBLE_CONFIG_SEED}; +use light_compressible::config::{CompressibleConfig, LIGHT_CONFIG_SEED}; /// Context for creating a compressible config #[derive(Accounts)] @@ -20,7 +20,7 @@ pub struct CreateCompressibleConfig<'info> { #[account( init, - seeds = [COMPRESSIBLE_CONFIG_SEED, &config_counter.counter.to_le_bytes()], + seeds = [LIGHT_CONFIG_SEED, &config_counter.counter.to_le_bytes()], bump, space = 8 + std::mem::size_of::(), payer = fee_payer, diff --git a/scripts/check-dependency-constraints.sh b/scripts/check-dependency-constraints.sh index 303b18e666..b0735592f9 100755 --- a/scripts/check-dependency-constraints.sh +++ b/scripts/check-dependency-constraints.sh @@ -14,7 +14,14 @@ SDK_LIBS_CRATES=( "light-sdk-macros" "light-sdk-pinocchio" "light-token" + "light-token-pinocchio" "light-token-types" + "light-token-client" + "light-account" + "light-account-pinocchio" + "light-compressed-token-sdk" + "light-instruction-decoder" + "light-instruction-decoder-derive" "light-client" "light-program-test" "light-event" diff --git a/scripts/lint.sh b/scripts/lint.sh index 4411c1c196..80dfd168d8 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -49,10 +49,18 @@ NO_DEFAULT_CRATES=( "aligned-sized" "light-sdk-types" "light-sdk-pinocchio" + "light-token-pinocchio" "light-sdk-macros" "light-token" "light-token-types" "light-sdk" + "light-account" + "light-account-pinocchio" + "light-client" + "light-compressed-token-sdk" + "light-instruction-decoder" + "light-program-test" + "light-token-client" "csdk-anchor-full-derived-test" ) diff --git a/sdk-libs/account-pinocchio/.cargo-rdme.toml b/sdk-libs/account-pinocchio/.cargo-rdme.toml new file mode 100644 index 0000000000..90a7c79d26 --- /dev/null +++ b/sdk-libs/account-pinocchio/.cargo-rdme.toml @@ -0,0 +1,2 @@ +workspace-project = "light-account-pinocchio" +heading-base-level = 0 diff --git a/sdk-libs/account-pinocchio/Cargo.toml b/sdk-libs/account-pinocchio/Cargo.toml new file mode 100644 index 0000000000..75b3f9f087 --- /dev/null +++ b/sdk-libs/account-pinocchio/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "light-account-pinocchio" +version = "0.1.0" +description = "Light Protocol account types with pinocchio AccountInfo specializations" +repository = "https://github.com/Lightprotocol/light-protocol" +license = "Apache-2.0" +edition = "2021" + +[features] +default = ["alloc"] +std = ["alloc", "light-sdk-types/std", "light-compressed-account/std", "light-account-checks/solana", "dep:solana-instruction", "dep:solana-pubkey"] +alloc = ["light-sdk-types/alloc", "light-compressed-account/alloc"] +token = ["light-sdk-types/token", "dep:light-token-interface"] +poseidon = ["light-sdk-types/poseidon", "light-hasher/poseidon"] +sha256 = ["light-sdk-types/sha256", "light-hasher/sha256"] + +[dependencies] +light-sdk-types = { workspace = true, default-features = false, features = ["alloc", "v2", "cpi-context"] } +light-sdk-macros = { workspace = true } +light-macros = { workspace = true } +light-account-checks = { workspace = true, default-features = false, features = ["pinocchio"] } +light-hasher = { workspace = true, default-features = false } +light-compressed-account = { workspace = true, default-features = false } +light-token-interface = { workspace = true, optional = true } +pinocchio = { workspace = true } +solana-instruction = { workspace = true, optional = true } +solana-pubkey = { workspace = true, optional = true } + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] diff --git a/sdk-libs/account-pinocchio/README.md b/sdk-libs/account-pinocchio/README.md new file mode 100644 index 0000000000..beeb2b9da6 --- /dev/null +++ b/sdk-libs/account-pinocchio/README.md @@ -0,0 +1,110 @@ + + +# Light Accounts Pinocchio + +Rent-free Light Accounts and Light Token Accounts for Pinocchio programs. + +## How It Works + +**Light Accounts (PDAs)** +1. Create a Solana PDA normally +2. Register it with `#[derive(LightProgramPinocchio)]` - becomes a Light Account +3. Use it as normal Solana account +4. When rent runs out, account compresses (cold state) +5. State preserved on-chain, client loads when needed (hot state) + +**Light Token Accounts (associated token accounts, Vaults)** +- Use `#[light_account(associated_token)]` for associated token accounts +- Use `#[light_account(token::seeds = [...], token::owner_seeds = [...])]` for vaults +- Cold/hot lifecycle + +**Light Mints** +- Created via `invoke_create_mints` +- Cold/hot lifecycle + +## Quick Start + +### 1. Program Setup + +```rust +use light_account_pinocchio::{derive_light_cpi_signer, CpiSigner, LightProgramPinocchio}; +use pinocchio_pubkey::pubkey; + +pub const ID: Pubkey = pubkey!("Your11111111111111111111111111111111111111"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("Your11111111111111111111111111111111111111"); +``` + +### 2. State Definition + +```rust +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{CompressionInfo, LightDiscriminator, LightHasherSha}; + +#[derive(BorshSerialize, BorshDeserialize, LightDiscriminator, LightHasherSha)] +pub struct MyRecord { + pub compression_info: CompressionInfo, // Required first or last field + pub owner: [u8; 32], + pub data: u64, +} +``` + +### 3. Program Accounts Enum + +```rust +#[derive(LightProgramPinocchio)] +pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"record", ctx.owner])] + MyRecord(MyRecord), +} +``` + +## Account Types + +### 1. Light Account (PDA) + +```rust +#[light_account(pda::seeds = [b"record", ctx.owner])] +MyRecord(MyRecord), +``` + +### 2. Light Account (zero-copy) + +```rust +#[light_account(pda::seeds = [b"record", ctx.owner], pda::zero_copy)] +ZeroCopyRecord(ZeroCopyRecord), +``` + +### 3. Light Token Account (vault) + +```rust +#[light_account(token::seeds = [b"vault", ctx.mint], token::owner_seeds = [b"vault_auth"])] +Vault, +``` + +### 4. Light Token Account (associated token account) + +```rust +#[light_account(associated_token)] +Ata, +``` + +## Required Derives + +| Derive | Use | +|--------|-----| +| `LightDiscriminator` | State structs (8-byte discriminator) | +| `LightHasherSha` | State structs (compression hashing) | +| `LightProgramPinocchio` | Program accounts enum | + +## Required Macros + +| Macro | Use | +|-------|-----| +| `derive_light_cpi_signer!` | CPI signer PDA constant | +| `pinocchio_pubkey::pubkey!` | Program ID as `Pubkey` | + +For a complete example, see `sdk-tests/pinocchio-light-program-test`. + + diff --git a/sdk-libs/account-pinocchio/src/lib.rs b/sdk-libs/account-pinocchio/src/lib.rs new file mode 100644 index 0000000000..66cf735f04 --- /dev/null +++ b/sdk-libs/account-pinocchio/src/lib.rs @@ -0,0 +1,313 @@ +//! # Light Accounts Pinocchio +//! +//! Rent-free Light Accounts and Light Token Accounts for Pinocchio programs. +//! +//! ## How It Works +//! +//! **Light Accounts (PDAs)** +//! 1. Create a Solana PDA normally +//! 2. Register it with `#[derive(LightProgramPinocchio)]` - becomes a Light Account +//! 3. Use it as normal Solana account +//! 4. When rent runs out, account compresses (cold state) +//! 5. State preserved on-chain, client loads when needed (hot state) +//! +//! **Light Token Accounts (associated token accounts, Vaults)** +//! - Use `#[light_account(associated_token)]` for associated token accounts +//! - Use `#[light_account(token::seeds = [...], token::owner_seeds = [...])]` for vaults +//! - Cold/hot lifecycle +//! +//! **Light Mints** +//! - Created via `invoke_create_mints` +//! - Cold/hot lifecycle +//! +//! ## Quick Start +//! +//! ### 1. Program Setup +//! +//! ```rust,ignore +//! use light_account_pinocchio::{derive_light_cpi_signer, CpiSigner, LightProgramPinocchio}; +//! use pinocchio_pubkey::pubkey; +//! +//! pub const ID: Pubkey = pubkey!("Your11111111111111111111111111111111111111"); +//! +//! pub const LIGHT_CPI_SIGNER: CpiSigner = +//! derive_light_cpi_signer!("Your11111111111111111111111111111111111111"); +//! ``` +//! +//! ### 2. State Definition +//! +//! ```rust,ignore +//! use borsh::{BorshDeserialize, BorshSerialize}; +//! use light_account_pinocchio::{CompressionInfo, LightDiscriminator, LightHasherSha}; +//! +//! #[derive(BorshSerialize, BorshDeserialize, LightDiscriminator, LightHasherSha)] +//! pub struct MyRecord { +//! pub compression_info: CompressionInfo, // Required first or last field +//! pub owner: [u8; 32], +//! pub data: u64, +//! } +//! ``` +//! +//! ### 3. Program Accounts Enum +//! +//! ```rust,ignore +//! #[derive(LightProgramPinocchio)] +//! pub enum ProgramAccounts { +//! #[light_account(pda::seeds = [b"record", ctx.owner])] +//! MyRecord(MyRecord), +//! } +//! ``` +//! +//! ## Account Types +//! +//! ### 1. Light Account (PDA) +//! +//! ```rust,ignore +//! #[light_account(pda::seeds = [b"record", ctx.owner])] +//! MyRecord(MyRecord), +//! ``` +//! +//! ### 2. Light Account (zero-copy) +//! +//! ```rust,ignore +//! #[light_account(pda::seeds = [b"record", ctx.owner], pda::zero_copy)] +//! ZeroCopyRecord(ZeroCopyRecord), +//! ``` +//! +//! ### 3. Light Token Account (vault) +//! +//! ```rust,ignore +//! #[light_account(token::seeds = [b"vault", ctx.mint], token::owner_seeds = [b"vault_auth"])] +//! Vault, +//! ``` +//! +//! ### 4. Light Token Account (associated token account) +//! +//! ```rust,ignore +//! #[light_account(associated_token)] +//! Ata, +//! ``` +//! +//! ## Required Derives +//! +//! | Derive | Use | +//! |--------|-----| +//! | `LightDiscriminator` | State structs (8-byte discriminator) | +//! | `LightHasherSha` | State structs (compression hashing) | +//! | `LightProgramPinocchio` | Program accounts enum | +//! +//! ## Required Macros +//! +//! | Macro | Use | +//! |-------|-----| +//! | `derive_light_cpi_signer!` | CPI signer PDA constant | +//! | `pinocchio_pubkey::pubkey!` | Program ID as `Pubkey` | +//! +//! For a complete example, see `sdk-tests/pinocchio-light-program-test`. + +pub use pinocchio::account_info::AccountInfo; + +// ===== TYPE ALIASES (structs generic over AI, specialized with pinocchio AccountInfo) ===== +// Note: pinocchio's AccountInfo has no lifetime parameter, so aliases have fewer lifetimes. + +pub type CpiAccounts<'c> = light_sdk_types::cpi_accounts::v2::CpiAccounts<'c, AccountInfo>; + +pub type CompressCtx<'a> = + light_sdk_types::interface::program::compression::processor::CompressCtx<'a, AccountInfo>; + +pub type CompressDispatchFn = + light_sdk_types::interface::program::compression::processor::CompressDispatchFn; + +pub type DecompressCtx<'a> = + light_sdk_types::interface::program::decompression::processor::DecompressCtx<'a, AccountInfo>; + +pub type ValidatedPdaContext = + light_sdk_types::interface::program::validation::ValidatedPdaContext; + +pub type CpiContextWriteAccounts<'a> = + light_sdk_types::cpi_context_write::CpiContextWriteAccounts<'a, AccountInfo>; + +#[cfg(all(not(target_os = "solana"), feature = "std"))] +pub type PackedAccounts = + light_sdk_types::pack_accounts::PackedAccounts; + +// ===== RE-EXPORTED TRAITS (generic over AI, used with explicit AccountInfo in impls) ===== + +pub use light_account_checks::close_account; +#[cfg(feature = "token")] +pub use light_compressed_account::instruction_data::compressed_proof::CompressedProof; +// ===== RE-EXPORTED CONCRETE TRAITS (no AI parameter) ===== +pub use light_sdk_types::interface::account::compression_info::{ + claim_completed_epoch_rent, CompressAs, CompressedAccountData, CompressedInitSpace, + CompressionInfo, CompressionInfoField, CompressionState, HasCompressionInfo, Space, + COMPRESSION_INFO_SIZE, OPTION_COMPRESSION_INFO_SPACE, +}; +#[cfg(all(not(target_os = "solana"), feature = "std"))] +pub use light_sdk_types::interface::account::pack::Pack; +// ===== TOKEN-GATED RE-EXPORTS ===== +#[cfg(feature = "token")] +pub use light_sdk_types::interface::account::token_seeds::{ + PackedTokenData, TokenDataWithPackedSeeds, TokenDataWithSeeds, +}; +// Mint creation CPI types and functions +#[cfg(feature = "token")] +pub use light_sdk_types::interface::cpi::create_mints::{ + derive_mint_compressed_address as derive_mint_compressed_address_generic, + get_output_queue_next_index, CreateMints, CreateMintsCpi, CreateMintsParams, + CreateMintsStaticAccounts, SingleMintParams, DEFAULT_RENT_PAYMENT, DEFAULT_WRITE_TOP_UP, +}; +// Token account/ATA creation CPI types and functions +#[cfg(feature = "token")] +pub use light_sdk_types::interface::cpi::create_token_accounts::{ + derive_associated_token_account as derive_associated_token_account_generic, + CreateTokenAccountCpi, CreateTokenAccountRentFreeCpi, CreateTokenAtaCpi, + CreateTokenAtaCpiIdempotent, CreateTokenAtaRentFreeCpi, +}; +// ===== RE-EXPORTED GENERIC FUNCTIONS (AI inferred from call-site args) ===== +pub use light_sdk_types::interface::cpi::invoke::invoke_light_system_program; +#[cfg(feature = "token")] +pub use light_sdk_types::interface::program::decompression::processor::process_decompress_accounts_idempotent; +#[cfg(feature = "token")] +pub use light_sdk_types::interface::program::decompression::token::prepare_token_account_for_decompression; +#[cfg(feature = "token")] +pub use light_sdk_types::interface::program::variant::{PackedTokenSeeds, UnpackedTokenSeeds}; +pub use light_sdk_types::interface::{ + account::{ + light_account::{AccountType, LightAccount}, + pack::Unpack, + pda_seeds::{HasTokenVariant, PdaSeedDerivation}, + size::Size, + }, + accounts::{ + finalize::{LightFinalize, LightPreInit}, + init_compressed_account::{prepare_compressed_account_on_init, reimburse_rent}, + }, + cpi::{ + account::CpiAccountsTrait, + invoke::{invoke_write_pdas_to_cpi_context, InvokeLightSystemProgram}, + LightCpi, + }, + create_accounts_proof::CreateAccountsProof, + program::{ + compression::{ + pda::prepare_account_for_compression, + processor::{process_compress_pda_accounts_idempotent, CompressAndCloseParams}, + }, + config::{ + create::process_initialize_light_config, process_initialize_light_config_checked, + process_update_light_config, InitializeLightConfigParams, LightConfig, + UpdateLightConfigParams, LIGHT_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE, + }, + decompression::{ + pda::prepare_account_for_decompression, + processor::{ + process_decompress_pda_accounts_idempotent, DecompressIdempotentParams, + DecompressVariant, + }, + }, + validation::{ + extract_tail_accounts, is_pda_initialized, should_skip_compression, + split_at_system_accounts_offset, validate_compress_accounts, + validate_decompress_accounts, + }, + variant::{IntoVariant, LightAccountVariantTrait, PackedLightAccountVariantTrait}, + }, + rent, +}; +#[cfg(feature = "token")] +pub use light_token_interface::instructions::extensions::ExtensionInstructionData as TokenExtensionInstructionData; +// Token-interface re-exports for macro-generated code +#[cfg(feature = "token")] +pub use light_token_interface::instructions::extensions::TokenMetadataInstructionData; + +#[cfg(feature = "token")] +pub mod token { + pub use light_sdk_types::interface::{ + account::token_seeds::{ + ExtensionInstructionData, MultiInputTokenDataWithContext, PackedTokenData, + TokenDataWithPackedSeeds, TokenDataWithSeeds, + }, + program::decompression::token::prepare_token_account_for_decompression, + }; +} + +pub mod compression_info { + pub use light_sdk_types::interface::account::compression_info::*; +} + +// ===== CPI / SDK-TYPES RE-EXPORTS ===== + +pub use light_sdk_types::cpi_accounts::CpiAccountsConfig; + +#[cfg(all(not(target_os = "solana"), feature = "std"))] +pub mod interface { + pub mod instruction { + pub use light_sdk_types::pack_accounts::PackedAccounts; + } +} + +pub mod account_meta { + pub use light_sdk_types::instruction::account_meta::*; +} + +// ===== ACCOUNT-CHECKS RE-EXPORTS (used by macro-generated code) ===== + +pub extern crate light_account_checks; +// ===== CONVENIENCE RE-EXPORTS ===== +pub use light_account_checks::{ + account_info::pinocchio::OwnedAccountMeta, discriminator::Discriminator as LightDiscriminator, + packed_accounts, AccountInfoTrait, AccountMetaTrait, +}; +pub use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +pub use light_macros::{derive_light_cpi_signer, derive_light_cpi_signer_pda}; +pub use light_sdk_macros::{ + AnchorDiscriminator as Discriminator, CompressAs, HasCompressionInfo, LightAccount, + LightDiscriminator, LightHasher, LightHasherSha, LightPinocchioAccount, LightProgramPinocchio, +}; +pub use light_sdk_types::{constants, error::LightSdkTypesError, instruction::*, CpiSigner}; + +// ===== UTILITY FUNCTIONS ===== + +/// Converts a [`LightSdkTypesError`] into a [`pinocchio::program_error::ProgramError`]. +/// +/// Use with `.map_err(light_err)` in pinocchio instruction handlers to disambiguate +/// the multiple `From` implementations on `LightSdkTypesError`. +pub fn light_err(e: LightSdkTypesError) -> pinocchio::program_error::ProgramError { + pinocchio::program_error::ProgramError::Custom(u32::from(e)) +} + +/// Derives the rent sponsor PDA for a given program. +/// +/// Seeds: `["rent_sponsor"]` +/// Returns `([u8; 32], u8)` since pinocchio uses raw byte array pubkeys. +pub fn derive_rent_sponsor_pda(program_id: &[u8; 32]) -> ([u8; 32], u8) { + ::find_program_address( + &[constants::RENT_SPONSOR_SEED], + program_id, + ) +} + +/// Find the mint PDA address for a given mint seed. +/// +/// Returns `([u8; 32], u8)` -- the PDA address and bump. +#[cfg(feature = "token")] +pub fn find_mint_address(mint_seed: &[u8; 32]) -> ([u8; 32], u8) { + light_sdk_types::interface::cpi::create_mints::find_mint_address::(mint_seed) +} + +/// Derive the compressed mint address from a mint seed and address tree pubkey. +#[cfg(feature = "token")] +pub fn derive_mint_compressed_address( + mint_seed: &[u8; 32], + address_tree_pubkey: &[u8; 32], +) -> [u8; 32] { + derive_mint_compressed_address_generic::(mint_seed, address_tree_pubkey) +} + +/// Derive the associated token account address for a given owner and mint. +/// +/// Returns `([u8; 32], u8)` -- the ATA address and bump seed. +#[cfg(feature = "token")] +pub fn derive_associated_token_account(owner: &[u8; 32], mint: &[u8; 32]) -> ([u8; 32], u8) { + derive_associated_token_account_generic::(owner, mint) +} diff --git a/sdk-libs/account/.cargo-rdme.toml b/sdk-libs/account/.cargo-rdme.toml new file mode 100644 index 0000000000..609877595a --- /dev/null +++ b/sdk-libs/account/.cargo-rdme.toml @@ -0,0 +1,2 @@ +workspace-project = "light-account" +heading-base-level = 0 diff --git a/sdk-libs/account/Cargo.toml b/sdk-libs/account/Cargo.toml new file mode 100644 index 0000000000..d6e54d5382 --- /dev/null +++ b/sdk-libs/account/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "light-account" +version = "0.1.0" +description = "Light Protocol account types with Solana AccountInfo specializations" +repository = "https://github.com/Lightprotocol/light-protocol" +license = "Apache-2.0" +edition = "2021" + +[features] +default = ["std"] +std = ["light-sdk-types/std", "light-compressed-account/std"] +alloc = ["light-sdk-types/alloc", "light-compressed-account/alloc"] +token = ["light-sdk-types/token", "dep:light-token-interface"] +poseidon = ["light-sdk-types/poseidon", "light-hasher/poseidon"] +sha256 = ["light-sdk-types/sha256", "light-hasher/sha256"] +anchor = ["light-sdk-types/anchor", "dep:anchor-lang"] + +[dependencies] +light-sdk-types = { workspace = true, features = ["std", "v2", "cpi-context"] } +light-sdk-macros = { workspace = true } +light-macros = { workspace = true } +light-account-checks = { workspace = true, features = ["solana"] } +light-hasher = { workspace = true, default-features = false } +light-compressed-account = { workspace = true } +light-compressible = { workspace = true } +light-token-interface = { workspace = true, optional = true } +anchor-lang = { workspace = true, optional = true } +solana-account-info = { workspace = true } +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] diff --git a/sdk-libs/account/README.md b/sdk-libs/account/README.md new file mode 100644 index 0000000000..ef67b38502 --- /dev/null +++ b/sdk-libs/account/README.md @@ -0,0 +1,179 @@ + + +# Light Accounts + +Rent-free Light Accounts and Light Token Accounts for Anchor programs. + +## How It Works + +**Light Accounts (PDAs)** +1. Create a Solana PDA normally (Anchor `init`) +2. Add `#[light_account(init)]` - becomes a Light Account +3. Use it as normal Solana account +4. When rent runs out, account compresses (cold state) +5. State preserved on-chain, client loads when needed (hot state) +6. When account is hot, use it as normal Solana account + +**Light Token Accounts (associated token accounts, Vaults)** +- Use `#[light_account(init, associated_token, ...)]` for associated token accounts +- Use `#[light_account(init, token, ...)]` for program-owned vaults +- Cold/hot lifecycle + +**Light Mints** +- Created via `CreateMintsCpi` +- Cold/hot lifecycle + +## Quick Start + +### 1. Program Setup + +```rust +use light_account::{derive_light_cpi_signer, light_program, CpiSigner}; + +declare_id!("Your11111111111111111111111111111111111111"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("Your11111111111111111111111111111111111111"); + +#[light_program] +#[program] +pub mod my_program { + // ... +} +``` + +### 2. State Definition + +```rust +use light_account::{CompressionInfo, LightAccount}; + +#[derive(Default, LightAccount)] +#[account] +pub struct UserRecord { + pub compression_info: CompressionInfo, // Required field + pub owner: Pubkey, + pub data: u64, +} +``` + +### 3. Accounts Struct + +```rust +use light_account::{CreateAccountsProof, LightAccounts}; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct CreateParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateParams)] +pub struct CreateRecord<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + /// CHECK: Rent sponsor + #[account(mut)] + pub pda_rent_sponsor: AccountInfo<'info>, + + #[account(init, payer = fee_payer, space = 8 + UserRecord::INIT_SPACE, seeds = [b"record", params.owner.as_ref()], bump)] + #[light_account(init)] + pub record: Account<'info, UserRecord>, + + pub system_program: Program<'info, System>, +} +``` + +## Account Types + +### 1. Light Account (PDA) + +```rust +#[account(init, payer = fee_payer, space = 8 + MyRecord::INIT_SPACE, seeds = [...], bump)] +#[light_account(init)] +pub record: Account<'info, MyRecord>, +``` + +### 2. Light Account (zero-copy) + +```rust +#[account(init, payer = fee_payer, space = 8 + size_of::(), seeds = [...], bump)] +#[light_account(init, zero_copy)] +pub record: AccountLoader<'info, MyZcRecord>, +``` + +### 3. Light Token Account (vault) + +**With `init` (Anchor-created):** +```rust +#[account(mut, seeds = [b"vault", mint.key().as_ref()], bump)] +#[light_account(init, token::seeds = [b"vault", self.mint.key()], token::owner_seeds = [b"vault_authority"])] +pub vault: UncheckedAccount<'info>, +``` + +**Without `init` (manual creation via `CreateTokenAccountCpi`):** +```rust +#[account(mut, seeds = [b"vault", mint.key().as_ref()], bump)] +#[light_account(token::seeds = [b"vault", self.mint.key()], token::owner_seeds = [b"vault_authority"])] +pub vault: UncheckedAccount<'info>, +``` + +### 4. Light Token Account (associated token account) + +**With `init` (Anchor-created):** +```rust +#[account(mut)] +#[light_account(init, associated_token::authority = owner, associated_token::mint = mint, associated_token::bump = params.bump)] +pub token_account: UncheckedAccount<'info>, +``` + +**Without `init` (manual creation via `CreateTokenAtaCpi`):** +```rust +#[account(mut)] +#[light_account(associated_token::authority = owner, associated_token::mint = mint)] +pub token_account: UncheckedAccount<'info>, +``` + +### 5. Light Mint + +```rust +#[account(mut)] +#[light_account(init, + mint::signer = mint_signer, // PDA that signs mint creation + mint::authority = mint_authority, // Mint authority + mint::decimals = 9, // Token decimals + mint::seeds = &[SEED, self.key.as_ref()], // Seeds for mint PDA + mint::bump = params.bump, // Bump seed + // Optional: PDA authority + mint::authority_seeds = &[b"authority"], + mint::authority_bump = params.auth_bump, + // Optional: Token metadata + mint::name = params.name, + mint::symbol = params.symbol, + mint::uri = params.uri, + mint::update_authority = update_auth, + mint::additional_metadata = params.metadata +)] +pub mint: UncheckedAccount<'info>, +``` + +## Required Derives + +| Derive | Use | +|--------|-----| +| `LightAccount` | State structs (must have `compression_info: CompressionInfo`) | +| `LightAccounts` | Accounts structs with `#[light_account(...)]` fields | + +## Required Macros + +| Macro | Use | +|-------|-----| +| `#[light_program]` | Program module (before `#[program]`) | +| `derive_light_cpi_signer!` | CPI signer PDA constant | +| `derive_light_rent_sponsor_pda!` | Rent sponsor PDA (optional) | + + diff --git a/sdk-libs/account/src/lib.rs b/sdk-libs/account/src/lib.rs new file mode 100644 index 0000000000..4f85448d18 --- /dev/null +++ b/sdk-libs/account/src/lib.rs @@ -0,0 +1,445 @@ +//! # Light Accounts +//! +//! Rent-free Light Accounts and Light Token Accounts for Anchor programs. +//! +//! ## How It Works +//! +//! **Light Accounts (PDAs)** +//! 1. Create a Solana PDA normally (Anchor `init`) +//! 2. Add `#[light_account(init)]` - becomes a Light Account +//! 3. Use it as normal Solana account +//! 4. When rent runs out, account compresses (cold state) +//! 5. State preserved on-chain, client loads when needed (hot state) +//! 6. When account is hot, use it as normal Solana account +//! +//! **Light Token Accounts (associated token accounts, Vaults)** +//! - Use `#[light_account(init, associated_token, ...)]` for associated token accounts +//! - Use `#[light_account(init, token, ...)]` for program-owned vaults +//! - Cold/hot lifecycle +//! +//! **Light Mints** +//! - Created via `CreateMintsCpi` +//! - Cold/hot lifecycle +//! +//! ## Quick Start +//! +//! ### 1. Program Setup +//! +//! ```rust,ignore +//! use light_account::{derive_light_cpi_signer, light_program, CpiSigner}; +//! +//! declare_id!("Your11111111111111111111111111111111111111"); +//! +//! pub const LIGHT_CPI_SIGNER: CpiSigner = +//! derive_light_cpi_signer!("Your11111111111111111111111111111111111111"); +//! +//! #[light_program] +//! #[program] +//! pub mod my_program { +//! // ... +//! } +//! ``` +//! +//! ### 2. State Definition +//! +//! ```rust,ignore +//! use light_account::{CompressionInfo, LightAccount}; +//! +//! #[derive(Default, LightAccount)] +//! #[account] +//! pub struct UserRecord { +//! pub compression_info: CompressionInfo, // Required field +//! pub owner: Pubkey, +//! pub data: u64, +//! } +//! ``` +//! +//! ### 3. Accounts Struct +//! +//! ```rust,ignore +//! use light_account::{CreateAccountsProof, LightAccounts}; +//! +//! #[derive(AnchorSerialize, AnchorDeserialize)] +//! pub struct CreateParams { +//! pub create_accounts_proof: CreateAccountsProof, +//! pub owner: Pubkey, +//! } +//! +//! #[derive(Accounts, LightAccounts)] +//! #[instruction(params: CreateParams)] +//! pub struct CreateRecord<'info> { +//! #[account(mut)] +//! pub fee_payer: Signer<'info>, +//! +//! /// CHECK: Compression config +//! pub compression_config: AccountInfo<'info>, +//! +//! /// CHECK: Rent sponsor +//! #[account(mut)] +//! pub pda_rent_sponsor: AccountInfo<'info>, +//! +//! #[account(init, payer = fee_payer, space = 8 + UserRecord::INIT_SPACE, seeds = [b"record", params.owner.as_ref()], bump)] +//! #[light_account(init)] +//! pub record: Account<'info, UserRecord>, +//! +//! pub system_program: Program<'info, System>, +//! } +//! ``` +//! +//! ## Account Types +//! +//! ### 1. Light Account (PDA) +//! +//! ```rust,ignore +//! #[account(init, payer = fee_payer, space = 8 + MyRecord::INIT_SPACE, seeds = [...], bump)] +//! #[light_account(init)] +//! pub record: Account<'info, MyRecord>, +//! ``` +//! +//! ### 2. Light Account (zero-copy) +//! +//! ```rust,ignore +//! #[account(init, payer = fee_payer, space = 8 + size_of::(), seeds = [...], bump)] +//! #[light_account(init, zero_copy)] +//! pub record: AccountLoader<'info, MyZcRecord>, +//! ``` +//! +//! ### 3. Light Token Account (vault) +//! +//! **With `init` (Anchor-created):** +//! ```rust,ignore +//! #[account(mut, seeds = [b"vault", mint.key().as_ref()], bump)] +//! #[light_account(init, token::seeds = [b"vault", self.mint.key()], token::owner_seeds = [b"vault_authority"])] +//! pub vault: UncheckedAccount<'info>, +//! ``` +//! +//! **Without `init` (manual creation via `CreateTokenAccountCpi`):** +//! ```rust,ignore +//! #[account(mut, seeds = [b"vault", mint.key().as_ref()], bump)] +//! #[light_account(token::seeds = [b"vault", self.mint.key()], token::owner_seeds = [b"vault_authority"])] +//! pub vault: UncheckedAccount<'info>, +//! ``` +//! +//! ### 4. Light Token Account (associated token account) +//! +//! **With `init` (Anchor-created):** +//! ```rust,ignore +//! #[account(mut)] +//! #[light_account(init, associated_token::authority = owner, associated_token::mint = mint, associated_token::bump = params.bump)] +//! pub token_account: UncheckedAccount<'info>, +//! ``` +//! +//! **Without `init` (manual creation via `CreateTokenAtaCpi`):** +//! ```rust,ignore +//! #[account(mut)] +//! #[light_account(associated_token::authority = owner, associated_token::mint = mint)] +//! pub token_account: UncheckedAccount<'info>, +//! ``` +//! +//! ### 5. Light Mint +//! +//! ```rust,ignore +//! #[account(mut)] +//! #[light_account(init, +//! mint::signer = mint_signer, // PDA that signs mint creation +//! mint::authority = mint_authority, // Mint authority +//! mint::decimals = 9, // Token decimals +//! mint::seeds = &[SEED, self.key.as_ref()], // Seeds for mint PDA +//! mint::bump = params.bump, // Bump seed +//! // Optional: PDA authority +//! mint::authority_seeds = &[b"authority"], +//! mint::authority_bump = params.auth_bump, +//! // Optional: Token metadata +//! mint::name = params.name, +//! mint::symbol = params.symbol, +//! mint::uri = params.uri, +//! mint::update_authority = update_auth, +//! mint::additional_metadata = params.metadata +//! )] +//! pub mint: UncheckedAccount<'info>, +//! ``` +//! +//! ## Required Derives +//! +//! | Derive | Use | +//! |--------|-----| +//! | `LightAccount` | State structs (must have `compression_info: CompressionInfo`) | +//! | `LightAccounts` | Accounts structs with `#[light_account(...)]` fields | +//! +//! ## Required Macros +//! +//! | Macro | Use | +//! |-------|-----| +//! | `#[light_program]` | Program module (before `#[program]`) | +//! | `derive_light_cpi_signer!` | CPI signer PDA constant | +//! | `derive_light_rent_sponsor_pda!` | Rent sponsor PDA (optional) | + +pub use solana_account_info::AccountInfo; + +// ===== TYPE ALIASES (structs generic over AI, specialized with AccountInfo) ===== + +pub type CpiAccounts<'c, 'info> = + light_sdk_types::cpi_accounts::v2::CpiAccounts<'c, AccountInfo<'info>>; + +pub type CompressCtx<'a, 'info> = + light_sdk_types::interface::program::compression::processor::CompressCtx< + 'a, + AccountInfo<'info>, + >; + +pub type CompressDispatchFn<'info> = + light_sdk_types::interface::program::compression::processor::CompressDispatchFn< + AccountInfo<'info>, + >; + +pub type DecompressCtx<'a, 'info> = + light_sdk_types::interface::program::decompression::processor::DecompressCtx< + 'a, + AccountInfo<'info>, + >; + +pub type ValidatedPdaContext<'info> = + light_sdk_types::interface::program::validation::ValidatedPdaContext>; + +#[cfg(not(target_os = "solana"))] +pub type PackedAccounts = + light_sdk_types::pack_accounts::PackedAccounts; + +// ===== RE-EXPORTED TRAITS (generic over AI, used with explicit AccountInfo in impls) ===== + +pub use light_account_checks::close_account; +#[cfg(feature = "token")] +pub use light_compressed_account::instruction_data::compressed_proof::CompressedProof; +// ===== RE-EXPORTED CONCRETE TRAITS (no AI parameter) ===== +pub use light_sdk_types::interface::account::compression_info::{ + claim_completed_epoch_rent, CompressAs, CompressedAccountData, CompressedInitSpace, + CompressionInfo, CompressionInfoField, CompressionState, HasCompressionInfo, Space, + COMPRESSION_INFO_SIZE, OPTION_COMPRESSION_INFO_SPACE, +}; +#[cfg(not(target_os = "solana"))] +pub use light_sdk_types::interface::account::pack::Pack; +// ===== TOKEN-GATED RE-EXPORTS ===== +#[cfg(feature = "token")] +pub use light_sdk_types::interface::account::token_seeds::{ + PackedTokenData, TokenDataWithPackedSeeds, TokenDataWithSeeds, +}; +// Mint creation CPI types and functions +#[cfg(feature = "token")] +pub use light_sdk_types::interface::cpi::create_mints::{ + derive_mint_compressed_address as derive_mint_compressed_address_generic, + get_output_queue_next_index, CreateMints, CreateMintsCpi, CreateMintsParams, + CreateMintsStaticAccounts, SingleMintParams, DEFAULT_RENT_PAYMENT, DEFAULT_WRITE_TOP_UP, +}; +// Token account/ATA creation CPI types and functions +#[cfg(feature = "token")] +pub use light_sdk_types::interface::cpi::create_token_accounts::{ + derive_associated_token_account as derive_associated_token_account_generic, + CreateTokenAccountCpi, CreateTokenAccountRentFreeCpi, CreateTokenAtaCpi, + CreateTokenAtaCpiIdempotent, CreateTokenAtaRentFreeCpi, +}; +// ===== RE-EXPORTED GENERIC FUNCTIONS (AI inferred from call-site args) ===== +pub use light_sdk_types::interface::cpi::invoke::invoke_light_system_program; +#[cfg(feature = "token")] +pub use light_sdk_types::interface::program::decompression::processor::process_decompress_accounts_idempotent; +#[cfg(feature = "token")] +pub use light_sdk_types::interface::program::decompression::token::prepare_token_account_for_decompression; +#[cfg(feature = "token")] +pub use light_sdk_types::interface::program::variant::{PackedTokenSeeds, UnpackedTokenSeeds}; +pub use light_sdk_types::interface::{ + account::{ + light_account::{AccountType, LightAccount}, + pack::Unpack, + pda_seeds::{HasTokenVariant, PdaSeedDerivation}, + }, + accounts::{ + finalize::{LightFinalize, LightPreInit}, + init_compressed_account::{prepare_compressed_account_on_init, reimburse_rent}, + }, + cpi::{ + account::CpiAccountsTrait, + invoke::{invoke_write_pdas_to_cpi_context, InvokeLightSystemProgram}, + LightCpi, + }, + create_accounts_proof::CreateAccountsProof, + program::{ + compression::{ + pda::prepare_account_for_compression, + processor::{process_compress_pda_accounts_idempotent, CompressAndCloseParams}, + }, + config::{ + process_initialize_light_config_checked, process_update_light_config, + InitializeLightConfigParams, LightConfig, UpdateLightConfigParams, LIGHT_CONFIG_SEED, + MAX_ADDRESS_TREES_PER_SPACE, + }, + decompression::{ + pda::prepare_account_for_decompression, + processor::{ + process_decompress_pda_accounts_idempotent, DecompressIdempotentParams, + DecompressVariant, + }, + }, + validation::{ + extract_tail_accounts, is_pda_initialized, should_skip_compression, + split_at_system_accounts_offset, validate_compress_accounts, + validate_decompress_accounts, + }, + variant::{IntoVariant, LightAccountVariantTrait, PackedLightAccountVariantTrait}, + }, + rent, +}; +#[cfg(feature = "token")] +pub use light_token_interface::instructions::extensions::ExtensionInstructionData as TokenExtensionInstructionData; +// Token-interface re-exports for macro-generated code +#[cfg(feature = "token")] +pub use light_token_interface::instructions::extensions::TokenMetadataInstructionData; +#[cfg(feature = "token")] +pub use light_token_interface::state::AdditionalMetadata; +/// Re-export Token state struct for client-side use. +#[cfg(feature = "token")] +pub use light_token_interface::state::{AccountState, Token}; + +/// Token sub-module for paths like `light_account::token::TokenDataWithSeeds`. +#[cfg(feature = "token")] +pub mod token { + pub use light_sdk_types::interface::{ + account::token_seeds::{ + ExtensionInstructionData, MultiInputTokenDataWithContext, PackedTokenData, + TokenDataWithPackedSeeds, TokenDataWithSeeds, + }, + program::decompression::token::prepare_token_account_for_decompression, + }; + pub use light_token_interface::state::{AccountState, Token}; +} + +/// Compression info sub-module for paths like `light_account::compression_info::CompressedInitSpace`. +pub mod compression_info { + pub use light_sdk_types::interface::account::compression_info::*; +} + +// ===== CPI / SDK-TYPES RE-EXPORTS ===== + +pub use light_sdk_types::{ + cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, + interface::program::config::create::process_initialize_light_config, +}; + +/// Sub-module for generic `PackedAccounts` (not specialized to AccountMeta). +#[cfg(not(target_os = "solana"))] +pub mod interface { + pub mod instruction { + pub use light_sdk_types::pack_accounts::PackedAccounts; + } +} + +/// Sub-module for account_meta types (e.g. `CompressedAccountMetaNoLamportsNoAddress`). +pub mod account_meta { + pub use light_sdk_types::instruction::account_meta::*; +} + +// ===== ACCOUNT-CHECKS RE-EXPORTS (used by macro-generated code) ===== + +/// Re-export `light_account_checks` so consumers can use `light_account::light_account_checks::*`. +pub extern crate light_account_checks; +// ===== CONVENIENCE RE-EXPORTS ===== +pub use light_account_checks::{ + discriminator::Discriminator as LightDiscriminator, packed_accounts, AccountInfoTrait, + AccountMetaTrait, +}; +pub use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +pub use light_compressible::rent::RentConfig; +pub use light_macros::{derive_light_cpi_signer, derive_light_cpi_signer_pda}; +pub use light_sdk_macros::{ + // Attribute macros + account, + // Proc macros + derive_light_rent_sponsor, + derive_light_rent_sponsor_pda, + light_program, + // Derive macros + AnchorDiscriminator as Discriminator, + CompressAs, + HasCompressionInfo, + LightAccount, + LightAccounts, + LightDiscriminator, + LightHasher, + LightHasherSha, + LightProgram, +}; +pub use light_sdk_types::{ + constants, + constants::{CPI_AUTHORITY_PDA_SEED, RENT_SPONSOR_SEED}, + error::LightSdkTypesError, + instruction::*, + interface::account::size::Size, + CpiSigner, +}; + +/// Hasher re-exports for macro-generated code paths like `light_account::hasher::DataHasher`. +pub mod hasher { + pub use light_hasher::{errors::HasherError, DataHasher, Hasher}; +} + +/// Re-export LIGHT_TOKEN_PROGRAM_ID as Pubkey for Anchor's `#[account(address = ...)]`. +pub const LIGHT_TOKEN_PROGRAM_ID: solana_pubkey::Pubkey = + solana_pubkey::Pubkey::new_from_array(constants::LIGHT_TOKEN_PROGRAM_ID); + +/// Default compressible config PDA for the Light Token Program. +pub const LIGHT_TOKEN_CONFIG: solana_pubkey::Pubkey = + solana_pubkey::Pubkey::new_from_array(constants::LIGHT_TOKEN_CONFIG); + +/// Default rent sponsor PDA for the Light Token Program. +pub const LIGHT_TOKEN_RENT_SPONSOR: solana_pubkey::Pubkey = + solana_pubkey::Pubkey::new_from_array(constants::LIGHT_TOKEN_RENT_SPONSOR); + +// ===== UTILITY FUNCTIONS ===== + +/// Converts a [`LightSdkTypesError`] into an [`anchor_lang::error::Error`]. +/// +/// Use with `.map_err(light_err)` in Anchor instruction handlers to disambiguate +/// the multiple `From` implementations on `LightSdkTypesError`. +#[cfg(feature = "anchor")] +pub fn light_err(e: LightSdkTypesError) -> anchor_lang::error::Error { + anchor_lang::error::Error::from(e) +} + +/// Derives the rent sponsor PDA for a given program. +/// +/// Seeds: `["rent_sponsor"]` +pub fn derive_rent_sponsor_pda(program_id: &solana_pubkey::Pubkey) -> (solana_pubkey::Pubkey, u8) { + solana_pubkey::Pubkey::find_program_address(&[constants::RENT_SPONSOR_SEED], program_id) +} + +/// Find the mint PDA address for a given mint seed. +/// +/// Returns `([u8; 32], u8)` -- the PDA address and bump. +#[cfg(feature = "token")] +pub fn find_mint_address(mint_seed: &[u8; 32]) -> ([u8; 32], u8) { + light_sdk_types::interface::cpi::create_mints::find_mint_address::>( + mint_seed, + ) +} + +/// Derive the compressed mint address from a mint seed and address tree pubkey. +#[cfg(feature = "token")] +pub fn derive_mint_compressed_address( + mint_seed: &[u8; 32], + address_tree_pubkey: &[u8; 32], +) -> [u8; 32] { + derive_mint_compressed_address_generic::>(mint_seed, address_tree_pubkey) +} + +/// Derive the associated token account address for a given owner and mint. +/// +/// Returns `(Pubkey, u8)` -- the ATA address and bump seed. +#[cfg(feature = "token")] +pub fn derive_associated_token_account( + owner: &solana_pubkey::Pubkey, + mint: &solana_pubkey::Pubkey, +) -> (solana_pubkey::Pubkey, u8) { + let (bytes, bump) = derive_associated_token_account_generic::>( + &owner.to_bytes(), + &mint.to_bytes(), + ); + (solana_pubkey::Pubkey::from(bytes), bump) +} diff --git a/sdk-libs/client/Cargo.toml b/sdk-libs/client/Cargo.toml index eac878dc51..f82338c49f 100644 --- a/sdk-libs/client/Cargo.toml +++ b/sdk-libs/client/Cargo.toml @@ -45,6 +45,7 @@ light-merkle-tree-metadata = { workspace = true, features = ["solana"] } light-concurrent-merkle-tree = { workspace = true } light-indexed-merkle-tree = { workspace = true } light-sdk = { workspace = true, features = ["v2", "cpi-context"] } +light-account = { workspace = true } light-hasher = { workspace = true, features = ["poseidon"] } light-compressed-account = { workspace = true, features = ["solana", "poseidon"] } light-token = { workspace = true, features = ["cpi-context"] } @@ -52,6 +53,7 @@ light-compressed-token-sdk = { workspace = true } light-token-interface = { workspace = true } light-event = { workspace = true } light-compressible = { workspace = true } +light-sdk-types = { workspace = true } photon-api = { workspace = true } light-prover-client = { workspace = true } diff --git a/sdk-libs/client/src/indexer/types.rs b/sdk-libs/client/src/indexer/types.rs index 3f653db274..2cd0f6c8d8 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -1,4 +1,5 @@ use borsh::BorshDeserialize; +use light_account::PackedAccounts; use light_compressed_account::{ compressed_account::{ CompressedAccount as ProgramCompressedAccount, CompressedAccountData, @@ -8,9 +9,7 @@ use light_compressed_account::{ TreeType, }; use light_indexed_merkle_tree::array::IndexedElement; -use light_sdk::instruction::{ - PackedAccounts, PackedAddressTreeInfo, PackedStateTreeInfo, ValidityProof, -}; +use light_sdk::instruction::{PackedAddressTreeInfo, PackedStateTreeInfo, ValidityProof}; use light_token::compat::{AccountState, TokenData}; use light_token_interface::state::ExtensionStruct; use num_bigint::BigUint; diff --git a/sdk-libs/client/src/interface/create_accounts_proof.rs b/sdk-libs/client/src/interface/create_accounts_proof.rs index 85a5d7f380..aaac726e38 100644 --- a/sdk-libs/client/src/interface/create_accounts_proof.rs +++ b/sdk-libs/client/src/interface/create_accounts_proof.rs @@ -92,7 +92,7 @@ impl CreateAccountsProofInput { } } -pub use light_compressible::CreateAccountsProof; +pub use light_sdk_types::interface::CreateAccountsProof; /// Result of `get_create_accounts_proof`. pub struct CreateAccountsProofResult { diff --git a/sdk-libs/client/src/interface/initialize_config.rs b/sdk-libs/client/src/interface/initialize_config.rs index a9145bf828..7b5919cdb1 100644 --- a/sdk-libs/client/src/interface/initialize_config.rs +++ b/sdk-libs/client/src/interface/initialize_config.rs @@ -4,7 +4,6 @@ use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; -use light_sdk::interface::config::LightConfig; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; @@ -88,7 +87,14 @@ impl InitializeRentFreeConfig { pub fn build(self) -> (Instruction, Pubkey) { let authority = self.authority.unwrap_or(self.fee_payer); - let (config_pda, _) = LightConfig::derive_pda(&self.program_id, self.config_bump); + let config_bump_u16 = self.config_bump as u16; + let (config_pda, _) = Pubkey::find_program_address( + &[ + light_account::LIGHT_CONFIG_SEED, + &config_bump_u16.to_le_bytes(), + ], + &self.program_id, + ); let accounts = vec![ AccountMeta::new(self.fee_payer, true), // payer diff --git a/sdk-libs/client/src/interface/instructions.rs b/sdk-libs/client/src/interface/instructions.rs index 702e231379..f6d754b9b1 100644 --- a/sdk-libs/client/src/interface/instructions.rs +++ b/sdk-libs/client/src/interface/instructions.rs @@ -4,12 +4,12 @@ use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; -use light_sdk::{ - compressible::{compression_info::CompressedAccountData, config::LightConfig, Pack}, - instruction::{ - account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, - SystemAccountMetaConfig, ValidityProof, - }, +use light_account::{ + CompressedAccountData, InitializeLightConfigParams, Pack, UpdateLightConfigParams, +}; +use light_sdk::instruction::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, + SystemAccountMetaConfig, ValidityProof, }; use light_token::constants::{ LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_CPI_AUTHORITY, LIGHT_TOKEN_PROGRAM_ID, @@ -29,20 +29,6 @@ fn get_output_queue(tree_info: &TreeInfo) -> Pubkey { .unwrap_or(tree_info.queue) } -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct InitializeConfigData { - pub rent_sponsor: Pubkey, - pub address_space: Vec, - pub config_bump: u8, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct UpdateConfigData { - pub new_rent_sponsor: Option, - pub new_address_space: Option>, - pub new_update_authority: Option, -} - #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct LoadAccountsData { pub system_accounts_offset: u8, @@ -114,7 +100,14 @@ pub fn initialize_config( config_bump: Option, ) -> Instruction { let config_bump = config_bump.unwrap_or(0); - let (config_pda, _) = LightConfig::derive_pda(program_id, config_bump); + let config_bump_u16 = config_bump as u16; + let (config_pda, _) = Pubkey::find_program_address( + &[ + light_account::LIGHT_CONFIG_SEED, + &config_bump_u16.to_le_bytes(), + ], + program_id, + ); let bpf_loader = solana_pubkey::pubkey!("BPFLoaderUpgradeab1e11111111111111111111111"); let (program_data_pda, _) = Pubkey::find_program_address(&[program_id.as_ref()], &bpf_loader); @@ -128,13 +121,15 @@ pub fn initialize_config( AccountMeta::new_readonly(system_program, false), ]; - let ix_data = InitializeConfigData { - rent_sponsor, - address_space, + let params = InitializeLightConfigParams { + rent_sponsor: rent_sponsor.to_bytes(), + compression_authority: authority.to_bytes(), + rent_config: Default::default(), + write_top_up: 0, + address_space: address_space.iter().map(|p| p.to_bytes()).collect(), config_bump, }; - - let serialized = ix_data.try_to_vec().expect("serialize"); + let serialized = params.try_to_vec().expect("serialize params"); let mut data = Vec::with_capacity(discriminator.len() + serialized.len()); data.extend_from_slice(discriminator); data.extend_from_slice(&serialized); @@ -154,20 +149,25 @@ pub fn update_config( new_address_space: Option>, new_update_authority: Option, ) -> Instruction { - let (config_pda, _) = LightConfig::derive_pda(program_id, 0); + let (config_pda, _) = Pubkey::find_program_address( + &[light_account::LIGHT_CONFIG_SEED, &0u16.to_le_bytes()], + program_id, + ); let accounts = vec![ AccountMeta::new(config_pda, false), AccountMeta::new_readonly(*authority, true), ]; - let ix_data = UpdateConfigData { - new_rent_sponsor, - new_address_space, - new_update_authority, + let params = UpdateLightConfigParams { + new_update_authority: new_update_authority.map(|p| p.to_bytes()), + new_rent_sponsor: new_rent_sponsor.map(|p| p.to_bytes()), + new_compression_authority: None, + new_rent_config: None, + new_write_top_up: None, + new_address_space: new_address_space.map(|v| v.iter().map(|p| p.to_bytes()).collect()), }; - - let serialized = ix_data.try_to_vec().expect("serialize"); + let serialized = params.try_to_vec().expect("serialize params"); let mut data = Vec::with_capacity(discriminator.len() + serialized.len()); data.extend_from_slice(discriminator); data.extend_from_slice(&serialized); @@ -190,7 +190,7 @@ pub fn create_decompress_accounts_idempotent_instruction( proof: ValidityProofWithContext, ) -> Result> where - T: Pack + Clone + std::fmt::Debug, + T: Pack + Clone + std::fmt::Debug, { if cold_accounts.is_empty() { return Err("cold_accounts cannot be empty".into()); @@ -280,10 +280,8 @@ where }; let serialized = ix_data.try_to_vec()?; - // Wrap in Vec format (4-byte length prefix) for Anchor compatibility - let mut data = Vec::with_capacity(discriminator.len() + 4 + serialized.len()); + let mut data = Vec::with_capacity(discriminator.len() + serialized.len()); data.extend_from_slice(discriminator); - data.extend_from_slice(&(serialized.len() as u32).to_le_bytes()); data.extend_from_slice(&serialized); Ok(Instruction { @@ -343,10 +341,8 @@ pub fn build_compress_accounts_idempotent( }; let serialized = ix_data.try_to_vec()?; - // Wrap in Vec format (4-byte length prefix) for Anchor compatibility - let mut data = Vec::with_capacity(discriminator.len() + 4 + serialized.len()); + let mut data = Vec::with_capacity(discriminator.len() + serialized.len()); data.extend_from_slice(discriminator); - data.extend_from_slice(&(serialized.len() as u32).to_le_bytes()); data.extend_from_slice(&serialized); Ok(Instruction { diff --git a/sdk-libs/client/src/interface/light_program_interface.rs b/sdk-libs/client/src/interface/light_program_interface.rs index 3817140a23..a1fa25ab0a 100644 --- a/sdk-libs/client/src/interface/light_program_interface.rs +++ b/sdk-libs/client/src/interface/light_program_interface.rs @@ -8,7 +8,7 @@ use std::fmt::Debug; -use light_sdk::interface::Pack; +use light_account::Pack; use light_token::instruction::derive_token_ata; use solana_pubkey::Pubkey; @@ -233,7 +233,7 @@ pub fn all_hot(specs: &[AccountSpec]) -> bool { /// Trait for programs to give clients a unified API to load cold program accounts. pub trait LightProgramInterface: Sized { /// The program's interface account variant enum. - type Variant: Pack + Clone + Debug; + type Variant: Pack + Clone + Debug; /// Program-specific instruction enum. type Instruction; diff --git a/sdk-libs/client/src/interface/load_accounts.rs b/sdk-libs/client/src/interface/load_accounts.rs index fbf4684d58..49e8bff452 100644 --- a/sdk-libs/client/src/interface/load_accounts.rs +++ b/sdk-libs/client/src/interface/load_accounts.rs @@ -1,5 +1,6 @@ //! Load cold accounts API. +use light_account::{derive_rent_sponsor_pda, Pack}; use light_compressed_account::{ compressed_account::PackedMerkleContext, instruction_data::compressed_proof::ValidityProof, }; @@ -9,7 +10,7 @@ use light_compressed_token_sdk::compressed_token::{ }, CTokenAccount2, }; -use light_sdk::{compressible::Pack, instruction::PackedAccounts, utils::derive_rent_sponsor_pda}; +use light_sdk::instruction::PackedAccounts; use light_token::{ compat::AccountState, instruction::{ @@ -81,7 +82,7 @@ pub async fn create_load_instructions( indexer: &I, ) -> Result, LoadAccountsError> where - V: Pack + Clone + std::fmt::Debug, + V: Pack + Clone + std::fmt::Debug, I: Indexer, { if !super::light_program_interface::any_cold(specs) { @@ -239,7 +240,7 @@ fn build_pda_load( compression_config: Pubkey, ) -> Result where - V: Pack + Clone + std::fmt::Debug, + V: Pack + Clone + std::fmt::Debug, { let has_tokens = specs.iter().any(|s| { s.compressed() diff --git a/sdk-libs/client/src/interface/mod.rs b/sdk-libs/client/src/interface/mod.rs index b8847c6e98..d3b5bd730c 100644 --- a/sdk-libs/client/src/interface/mod.rs +++ b/sdk-libs/client/src/interface/mod.rs @@ -21,12 +21,12 @@ pub use decompress_mint::{ DecompressMintError, MintInterface, MintState, DEFAULT_RENT_PAYMENT, DEFAULT_WRITE_TOP_UP, }; pub use initialize_config::InitializeRentFreeConfig; -pub use light_compressible::CreateAccountsProof; +pub use light_account::LightConfig; pub use light_program_interface::{ all_hot, any_cold, discriminator, matches_discriminator, AccountSpec, AccountToFetch, ColdContext, LightProgramInterface, PdaSpec, }; -pub use light_sdk::interface::config::LightConfig; +pub use light_sdk_types::interface::CreateAccountsProof; pub use light_token::compat::TokenData; pub use load_accounts::{create_load_instructions, LoadAccountsError}; pub use pack::{pack_proof, PackError, PackedProofResult}; diff --git a/sdk-libs/compressed-token-sdk/Cargo.toml b/sdk-libs/compressed-token-sdk/Cargo.toml index 1c2d0cf63c..000b558446 100644 --- a/sdk-libs/compressed-token-sdk/Cargo.toml +++ b/sdk-libs/compressed-token-sdk/Cargo.toml @@ -31,6 +31,7 @@ light-token-types = { workspace = true } light-compressed-account = { workspace = true, features = ["std", "solana"] } light-token-interface = { workspace = true } light-sdk = { workspace = true, features = ["v2"] } +light-account = { workspace = true } light-sdk-types = { workspace = true, features = ["v2"] } light-account-checks = { workspace = true, features = ["solana"] } light-zero-copy = { workspace = true } diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs index 7e5d7a636d..8978d2703c 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/compress_and_close.rs @@ -1,10 +1,13 @@ +#[cfg(not(target_os = "solana"))] +use light_account::PackedAccounts; use light_program_profiler::profile; // PackedAccounts and AccountMetasVec are only available off-chain (client-side) #[cfg(not(target_os = "solana"))] -use light_sdk::{ - error::LightSdkError, - instruction::{AccountMetasVec, PackedAccounts, SystemAccountMetaConfig}, +use light_sdk::instruction::{ + get_light_system_account_metas_v2, AccountMetasVec, SystemAccountMetaConfig, }; +#[cfg(not(target_os = "solana"))] +use light_sdk_types::error::LightSdkTypesError; use light_token_interface::instructions::transfer2::CompressedCpiContext; #[cfg(not(target_os = "solana"))] use light_token_interface::state::Token; @@ -398,11 +401,14 @@ impl CompressAndCloseAccounts { } #[cfg(not(target_os = "solana"))] -impl AccountMetasVec for CompressAndCloseAccounts { +impl AccountMetasVec for CompressAndCloseAccounts { /// Adds: /// 1. system accounts if not set /// 2. compressed token program and ctoken cpi authority pda to pre accounts - fn get_account_metas_vec(&self, accounts: &mut PackedAccounts) -> Result<(), LightSdkError> { + fn get_account_metas_vec( + &self, + accounts: &mut PackedAccounts, + ) -> Result<(), LightSdkTypesError> { if !accounts.system_accounts_set() { let mut config = SystemAccountMetaConfig::default(); config.self_program = self.self_program; @@ -414,10 +420,10 @@ impl AccountMetasVec for CompressAndCloseAccounts { { if self.cpi_context.is_some() { msg!("Error: cpi_context is set but 'cpi-context' feature is not enabled"); - return Err(LightSdkError::ExpectedCpiContext); + return Err(LightSdkTypesError::InvalidInstructionData); } } - accounts.add_system_accounts_v2(config)?; + accounts.add_system_accounts_raw(get_light_system_account_metas_v2(config)); } // Add both accounts in one operation for better performance accounts.pre_accounts.extend_from_slice(&[ diff --git a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/decompress_full.rs b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/decompress_full.rs index f6346cabad..2cd8830ce8 100644 --- a/sdk-libs/compressed-token-sdk/src/compressed_token/v2/decompress_full.rs +++ b/sdk-libs/compressed-token-sdk/src/compressed_token/v2/decompress_full.rs @@ -1,16 +1,17 @@ +use light_account::Unpack; +// Pack and PackedAccounts only available off-chain (client-side) +#[cfg(not(target_os = "solana"))] +use light_account::{Pack, PackedAccounts}; #[cfg(not(target_os = "solana"))] use light_compressed_account::compressed_account::PackedMerkleContext; use light_compressed_account::instruction_data::compressed_proof::ValidityProof; use light_program_profiler::profile; +use light_sdk::instruction::PackedStateTreeInfo; #[cfg(not(target_os = "solana"))] -use light_sdk::error::LightSdkError; -use light_sdk::{instruction::PackedStateTreeInfo, Unpack}; -// Pack and PackedAccounts only available off-chain (client-side) -#[cfg(not(target_os = "solana"))] -use light_sdk::{ - instruction::{AccountMetasVec, PackedAccounts, SystemAccountMetaConfig}, - Pack, +use light_sdk::instruction::{ + get_light_system_account_metas_v2, AccountMetasVec, SystemAccountMetaConfig, }; +use light_sdk_types::error::LightSdkTypesError; use light_token_interface::instructions::{ extensions::ExtensionInstructionData, transfer2::{CompressedCpiContext, MultiInputTokenDataWithContext}, @@ -59,13 +60,13 @@ pub struct DecompressFullInput { } #[cfg(not(target_os = "solana"))] -impl Pack for DecompressFullInput { +impl Pack for DecompressFullInput { type Packed = DecompressFullIndices; fn pack( &self, remaining_accounts: &mut PackedAccounts, - ) -> Result { + ) -> Result { let owner_is_signer = !self.is_ata; let source = MultiInputTokenDataWithContext { @@ -101,26 +102,26 @@ impl Pack for DecompressFullInput { } } -impl Unpack for DecompressFullIndices { +impl<'a> Unpack> for DecompressFullIndices { type Unpacked = DecompressFullInput; fn unpack( &self, - remaining_accounts: &[AccountInfo], - ) -> Result { + remaining_accounts: &[AccountInfo<'a>], + ) -> Result { let owner = *remaining_accounts .get(self.source.owner as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .ok_or(LightSdkTypesError::InvalidInstructionData)? .key; let mint = *remaining_accounts .get(self.source.mint as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .ok_or(LightSdkTypesError::InvalidInstructionData)? .key; let delegate = if self.source.has_delegate { Some( *remaining_accounts .get(self.source.delegate as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .ok_or(LightSdkTypesError::InvalidInstructionData)? .key, ) } else { @@ -128,7 +129,7 @@ impl Unpack for DecompressFullIndices { }; let destination = *remaining_accounts .get(self.destination_index as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .ok_or(LightSdkTypesError::InvalidInstructionData)? .key; Ok(DecompressFullInput { @@ -350,11 +351,14 @@ impl DecompressFullAccounts { } #[cfg(not(target_os = "solana"))] -impl AccountMetasVec for DecompressFullAccounts { +impl AccountMetasVec for DecompressFullAccounts { /// Adds: /// 1. system accounts if not set /// 2. compressed token program and ctoken cpi authority pda to pre accounts - fn get_account_metas_vec(&self, accounts: &mut PackedAccounts) -> Result<(), LightSdkError> { + fn get_account_metas_vec( + &self, + accounts: &mut PackedAccounts, + ) -> Result<(), LightSdkTypesError> { if !accounts.system_accounts_set() { #[cfg(feature = "cpi-context")] let config = { @@ -370,7 +374,7 @@ impl AccountMetasVec for DecompressFullAccounts { config }; - accounts.add_system_accounts_v2(config)?; + accounts.add_system_accounts_raw(get_light_system_account_metas_v2(config)); } // Add both accounts in one operation for better performance accounts.pre_accounts.extend_from_slice(&[ diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 801155f79a..878d55739b 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -250,56 +250,63 @@ pub fn light_program(args: TokenStream, input: TokenStream) -> TokenStream { into_token_stream(light_pdas::program::light_program_impl(args.into(), module)) } -#[proc_macro_attribute] -pub fn account(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - into_token_stream(account::account(input)) -} - -/// Automatically implements all required traits for compressible accounts. +/// Derive macro for manually specifying compressed account variants on an enum. /// -/// This derive macro generates HasCompressionInfo, Size, and CompressAs trait implementations. -/// It supports optional compress_as attribute for custom compression behavior. +/// Generates equivalent code to `#[light_program]` auto-discovery, but allows +/// specifying account types and seeds explicitly. Useful for external programs +/// where you don't own the module. /// -/// ## Example - Basic Usage +/// ## Example /// /// ```ignore -/// use light_sdk_macros::Compressible; -/// use light_compressible::CompressionInfo; -/// use solana_pubkey::Pubkey; +/// #[derive(LightProgram)] +/// pub enum ProgramAccounts { +/// #[light_account(pda::seeds = [b"record", ctx.owner])] +/// Record(MinimalRecord), /// -/// #[derive(Compressible)] -/// pub struct UserRecord { -/// pub compression_info: Option, -/// pub owner: Pubkey, -/// pub name: String, -/// pub score: u64, +/// #[light_account(pda::seeds = [RECORD_SEED, ctx.owner], pda::zero_copy)] +/// ZeroCopyRecord(ZeroCopyRecord), +/// +/// #[light_account(token::seeds = [VAULT_SEED, ctx.mint], token::owner_seeds = [AUTH_SEED])] +/// Vault, +/// +/// #[light_account(associated_token)] +/// Ata, /// } /// ``` /// -/// ## Example - Custom Compression +/// Seed expressions use explicit prefixes: +/// - `ctx.field` - context account reference +/// - `data.field` - instruction data parameter +/// - `b"literal"` or `"literal"` - byte/string literal +/// - `CONSTANT` or `path::CONSTANT` - constant in SCREAMING_SNAKE_CASE +#[proc_macro_derive(LightProgram, attributes(light_account))] +pub fn light_program_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + into_token_stream(light_pdas::program::derive_light_program_impl(input)) +} + +/// Pinocchio variant of `#[derive(LightProgram)]`. /// -/// ```ignore -/// use light_sdk_macros::Compressible; -/// use light_compressible::CompressionInfo; -/// use solana_pubkey::Pubkey; +/// Generates pinocchio-compatible code instead of Anchor: +/// - `BorshSerialize/BorshDeserialize` instead of `AnchorSerialize/AnchorDeserialize` +/// - `light_account_pinocchio::` paths instead of `light_account::` +/// - Config/compress/decompress as enum associated functions +/// - `[u8; 32]` instead of `Pubkey` in generated params /// -/// #[derive(Compressible)] -/// #[compress_as(start_time = 0, end_time = None, score = 0)] -/// pub struct GameSession { -/// pub compression_info: Option, -/// pub session_id: u64, // KEPT -/// pub player: Pubkey, // KEPT -/// pub game_type: String, // KEPT -/// pub start_time: u64, // RESET to 0 -/// pub end_time: Option, // RESET to None -/// pub score: u64, // RESET to 0 -/// } -/// ``` -#[proc_macro_derive(Compressible, attributes(compress_as, light_seeds))] -pub fn compressible_derive(input: TokenStream) -> TokenStream { +/// See `#[derive(LightProgram)]` for usage syntax (identical attribute syntax). +#[proc_macro_derive(LightProgramPinocchio, attributes(light_account))] +pub fn light_program_pinocchio_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(light_pdas::account::traits::derive_compressible(input)) + into_token_stream(light_pdas::program::derive_light_program_pinocchio_impl( + input, + )) +} + +#[proc_macro_attribute] +pub fn account(_: TokenStream, input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as ItemStruct); + into_token_stream(account::account(input)) } /// Generates a unified `LightAccount` trait implementation for light account structs. @@ -314,7 +321,7 @@ pub fn compressible_derive(input: TokenStream) -> TokenStream { /// /// ```ignore /// use light_sdk_macros::{LightAccount, LightDiscriminator, LightHasherSha}; -/// use light_sdk::compressible::CompressionInfo; +/// use light_account::CompressionInfo; /// use solana_pubkey::Pubkey; /// /// #[derive(Default, Debug, InitSpace, LightAccount, LightDiscriminator, LightHasherSha)] @@ -356,6 +363,46 @@ pub fn light_account_derive(input: TokenStream) -> TokenStream { into_token_stream(light_pdas::account::derive::derive_light_account(input)) } +/// Pinocchio variant of `LightAccount` derive macro. +/// +/// Same as `#[derive(LightAccount)]` but generates pinocchio-compatible code: +/// - Uses `BorshSerialize/BorshDeserialize` instead of Anchor serialization +/// - Uses `light_account_pinocchio::` paths for on-chain code +/// - Uses `core::mem::size_of::()` for INIT_SPACE (always zero-copy style) +/// +/// ## Example +/// +/// ```ignore +/// use light_sdk_macros::{LightPinocchioAccount, LightDiscriminator, LightHasherSha}; +/// use light_account_pinocchio::CompressionInfo; +/// +/// #[derive( +/// Default, Debug, Clone, PartialEq, +/// BorshSerialize, BorshDeserialize, +/// LightDiscriminator, LightHasherSha, +/// LightPinocchioAccount, +/// )] +/// #[repr(C)] +/// pub struct MinimalRecord { +/// pub compression_info: CompressionInfo, +/// pub owner: [u8; 32], +/// } +/// ``` +/// +/// ## Requirements +/// +/// - The `compression_info` field must be non-Option `CompressionInfo` type +/// - The `compression_info` field must be first or last field in the struct +/// - Struct should be `#[repr(C)]` for predictable memory layout +/// - Use `[u8; 32]` instead of `Pubkey` for address fields +#[proc_macro_derive(LightPinocchioAccount, attributes(compress_as, skip))] +pub fn light_pinocchio_account_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + into_token_stream(light_pdas::account::derive::derive_light_pinocchio_account( + input, + )) +} + /// Derives a Rent Sponsor PDA for a program at compile time. /// /// Seeds: ["rent_sponsor"] @@ -398,7 +445,7 @@ pub fn derive_light_rent_sponsor(input: TokenStream) -> TokenStream { /// - Accounts marked with `#[light_account(init, mint, ...)]` (compressed mints) /// - Accounts marked with `#[light_account(token, ...)]` (rent-free token accounts) /// -/// The trait is defined in `light_sdk::interface::LightFinalize`. +/// The trait is defined in `light_account::LightFinalize`. /// /// ## Usage - PDAs /// diff --git a/sdk-libs/macros/src/light_pdas/account/derive.rs b/sdk-libs/macros/src/light_pdas/account/derive.rs index b751bf54b0..c7fcde8777 100644 --- a/sdk-libs/macros/src/light_pdas/account/derive.rs +++ b/sdk-libs/macros/src/light_pdas/account/derive.rs @@ -12,6 +12,33 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{punctuated::Punctuated, DeriveInput, Field, Fields, Ident, ItemStruct, Result, Token}; +/// Target framework for generated code. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Framework { + Anchor, + Pinocchio, +} + +impl Framework { + /// Crate path prefix for on-chain code (LightAccount trait, AccountType, etc.) + fn on_chain_crate(&self) -> TokenStream { + match self { + Framework::Anchor => quote! { light_account }, + Framework::Pinocchio => quote! { light_account_pinocchio }, + } + } + + /// Serialization derives for packed struct. + fn serde_derives(&self) -> TokenStream { + match self { + Framework::Anchor => { + quote! { anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize } + } + Framework::Pinocchio => quote! { borsh::BorshSerialize, borsh::BorshDeserialize }, + } + } +} + use super::{ traits::{parse_compress_as_overrides, CompressAsFields}, validation::validate_compression_info_field, @@ -37,7 +64,7 @@ fn is_zero_copy(attrs: &[syn::Attribute]) -> bool { }) } -/// Derives all required traits for a compressible account. +/// Derives all required traits for a compressible account (Anchor variant). /// /// This generates: /// - `LightHasherSha` - SHA256-based DataHasher and ToByteArray implementations @@ -55,7 +82,7 @@ fn is_zero_copy(attrs: &[syn::Attribute]) -> bool { /// /// ```ignore /// use light_sdk_macros::{LightAccount, LightDiscriminator, LightHasherSha}; -/// use light_sdk::compressible::CompressionInfo; +/// use light_account::CompressionInfo; /// use solana_pubkey::Pubkey; /// /// #[derive(Default, Debug, InitSpace, LightAccount, LightDiscriminator, LightHasherSha)] @@ -77,6 +104,21 @@ fn is_zero_copy(attrs: &[syn::Attribute]) -> bool { /// - Use `#[compress_as(field = value)]` to override field values during compression /// - Use `#[skip]` to exclude fields from compression entirely pub fn derive_light_account(input: DeriveInput) -> Result { + derive_light_account_internal(input, Framework::Anchor) +} + +/// Derives all required traits for a compressible account (Pinocchio variant). +/// +/// Same as `derive_light_account` but generates pinocchio-compatible code: +/// - Uses `BorshSerialize/BorshDeserialize` instead of Anchor serialization +/// - Uses `light_account_pinocchio::` paths for on-chain code +/// - Uses `core::mem::size_of::()` for INIT_SPACE +pub fn derive_light_pinocchio_account(input: DeriveInput) -> Result { + derive_light_account_internal(input, Framework::Pinocchio) +} + +/// Internal implementation of LightAccount derive, parameterized by framework. +fn derive_light_account_internal(input: DeriveInput, framework: Framework) -> Result { // Convert DeriveInput to ItemStruct for macros that need it let item_struct = derive_input_to_item_struct(&input)?; @@ -87,13 +129,14 @@ pub fn derive_light_account(input: DeriveInput) -> Result { let discriminator_impl = discriminator::anchor_discriminator(item_struct)?; // Generate unified LightAccount implementation (includes PackedXxx struct) - let light_account_impl = generate_light_account_impl(&input)?; + let light_account_impl = generate_light_account_impl(&input, framework)?; - // For zero-copy (Pod) types, generate AnchorSerialize/AnchorDeserialize impls + // For zero-copy (Pod) types with Anchor, generate AnchorSerialize/AnchorDeserialize impls // using fully-qualified anchor_lang:: paths. This is necessary because the workspace // borsh dependency resolves to a different crate instance than anchor_lang's borsh // (due to proc-macro boundary causing crate duplication). - let anchor_serde_impls = if is_zero_copy(&input.attrs) { + // For Pinocchio, we don't generate these - the struct should already derive BorshSerialize/BorshDeserialize. + let anchor_serde_impls = if framework == Framework::Anchor && is_zero_copy(&input.attrs) { generate_anchor_serde_for_zero_copy(&input)? } else { quote! {} @@ -187,7 +230,7 @@ fn generate_anchor_serde_for_zero_copy(input: &DeriveInput) -> Result Result { +fn generate_light_account_impl(input: &DeriveInput, framework: Framework) -> Result { let struct_name = &input.ident; let packed_struct_name = format_ident!("Packed{}", struct_name); let fields = extract_fields_from_derive_input(input)?; @@ -212,158 +255,189 @@ fn generate_light_account_impl(input: &DeriveInput) -> Result { .any(|f| is_pubkey_type(&f.ty)); // Generate the packed struct (excludes compression_info) - let packed_struct = generate_packed_struct(&packed_struct_name, fields, has_pubkey_fields)?; + let packed_struct = + generate_packed_struct(&packed_struct_name, fields, has_pubkey_fields, framework)?; - // Generate pack method body - let pack_body = generate_pack_body(&packed_struct_name, fields, has_pubkey_fields)?; + // Generate pack method body (off-chain) + let pack_body = generate_pack_body(&packed_struct_name, fields, has_pubkey_fields, framework)?; - // Generate unpack method body - let unpack_body = generate_unpack_body(struct_name, fields, has_pubkey_fields)?; + // Generate unpack method body (on-chain, uses framework-specific paths) + let unpack_body = generate_unpack_body(struct_name, fields, has_pubkey_fields, framework)?; // Generate compress_as body for set_decompressed - let compress_as_assignments = generate_compress_as_assignments(fields, &compress_as_fields); + let compress_as_assignments = + generate_compress_as_assignments(fields, &compress_as_fields, framework); // Generate compress_as impl body for CompressAs trait - let compress_as_impl_body = generate_compress_as_impl_body(fields, &compress_as_fields); - - // Generate the 800-byte size assertion and account type based on zero-copy mode - let (size_assertion, account_type_token, init_space_token) = if is_zero_copy { - ( - quote! { - const _: () = { - assert!( - core::mem::size_of::<#struct_name>() <= 800, - "Compressed account size exceeds 800 byte limit" - ); - }; - }, - quote! { light_sdk::interface::AccountType::PdaZeroCopy }, - quote! { core::mem::size_of::() }, - ) - } else { - ( - quote! { - const _: () = { - assert!( - <#struct_name as anchor_lang::Space>::INIT_SPACE <= 800, - "Compressed account size exceeds 800 byte limit" - ); - }; - }, - quote! { light_sdk::interface::AccountType::Pda }, - quote! { ::INIT_SPACE }, - ) + let compress_as_impl_body = + generate_compress_as_impl_body(fields, &compress_as_fields, framework); + + // Get the on-chain crate path (light_account or light_account_pinocchio) + let on_chain_crate = framework.on_chain_crate(); + + // Generate the 800-byte size assertion and account type based on framework and zero-copy mode + let (size_assertion, account_type_token, init_space_token) = match framework { + Framework::Pinocchio => { + // Pinocchio always uses core::mem::size_of and PdaZeroCopy + ( + quote! { + const _: () = { + assert!( + core::mem::size_of::<#struct_name>() <= 800, + "Compressed account size exceeds 800 byte limit" + ); + }; + }, + quote! { #on_chain_crate::AccountType::PdaZeroCopy }, + quote! { core::mem::size_of::() }, + ) + } + Framework::Anchor => { + if is_zero_copy { + ( + quote! { + const _: () = { + assert!( + core::mem::size_of::<#struct_name>() <= 800, + "Compressed account size exceeds 800 byte limit" + ); + }; + }, + quote! { #on_chain_crate::AccountType::PdaZeroCopy }, + quote! { core::mem::size_of::() }, + ) + } else { + ( + quote! { + const _: () = { + assert!( + <#struct_name as anchor_lang::Space>::INIT_SPACE <= 800, + "Compressed account size exceeds 800 byte limit" + ); + }; + }, + quote! { #on_chain_crate::AccountType::Pda }, + quote! { ::INIT_SPACE }, + ) + } + } }; // Generate the LightAccount impl + // Note: pack is off-chain only, uses light_account:: paths + // unpack is on-chain, uses framework-specific paths let light_account_impl = quote! { #packed_struct #size_assertion - impl light_sdk::interface::LightAccount for #struct_name { - const ACCOUNT_TYPE: light_sdk::interface::AccountType = #account_type_token; + impl #on_chain_crate::LightAccount for #struct_name { + const ACCOUNT_TYPE: #on_chain_crate::AccountType = #account_type_token; type Packed = #packed_struct_name; const INIT_SPACE: usize = #init_space_token; #[inline] - fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + fn compression_info(&self) -> &#on_chain_crate::CompressionInfo { &self.compression_info } #[inline] - fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { + fn compression_info_mut(&mut self) -> &mut #on_chain_crate::CompressionInfo { &mut self.compression_info } - fn set_decompressed(&mut self, config: &light_sdk::interface::LightConfig, current_slot: u64) { - self.compression_info = light_sdk::compressible::CompressionInfo::new_from_config(config, current_slot); + fn set_decompressed(&mut self, config: &#on_chain_crate::LightConfig, current_slot: u64) { + self.compression_info = #on_chain_crate::CompressionInfo::new_from_config(config, current_slot); #compress_as_assignments } + // pack is off-chain only (client-side) + #[cfg(not(target_os = "solana"))] #[inline(never)] - fn pack( + fn pack( &self, - accounts: &mut light_sdk::instruction::PackedAccounts, - ) -> std::result::Result { + accounts: &mut #on_chain_crate::interface::instruction::PackedAccounts, + ) -> std::result::Result { #pack_body } + // unpack is on-chain - uses framework-specific paths #[inline(never)] - fn unpack( + fn unpack( packed: &Self::Packed, - accounts: &light_sdk::light_account_checks::packed_accounts::ProgramPackedAccounts, - ) -> std::result::Result { + accounts: &#on_chain_crate::packed_accounts::ProgramPackedAccounts, + ) -> std::result::Result { #unpack_body } } // V1 compatibility: Pack trait (delegates to LightAccount::pack) - // Pack trait is only available off-chain (client-side) + // Pack trait is off-chain only (client-side) #[cfg(not(target_os = "solana"))] - impl light_sdk::interface::Pack for #struct_name { + impl #on_chain_crate::Pack for #struct_name { type Packed = #packed_struct_name; fn pack( &self, - remaining_accounts: &mut light_sdk::instruction::PackedAccounts, - ) -> std::result::Result { - ::pack(self, remaining_accounts) + remaining_accounts: &mut #on_chain_crate::interface::instruction::PackedAccounts, + ) -> std::result::Result { + ::pack(self, remaining_accounts) } } // V1 compatibility: Unpack trait for packed struct - impl light_sdk::interface::Unpack for #packed_struct_name { + // Uses framework-specific paths for on-chain code + impl #on_chain_crate::Unpack for #packed_struct_name { type Unpacked = #struct_name; fn unpack( &self, - remaining_accounts: &[solana_account_info::AccountInfo], - ) -> std::result::Result { + remaining_accounts: &[AI], + ) -> std::result::Result { // Create a ProgramPackedAccounts wrapper from remaining_accounts - let accounts = light_sdk::light_account_checks::packed_accounts::ProgramPackedAccounts { + let accounts = #on_chain_crate::packed_accounts::ProgramPackedAccounts { accounts: remaining_accounts }; - <#struct_name as light_sdk::interface::LightAccount>::unpack(self, &accounts) + <#struct_name as #on_chain_crate::LightAccount>::unpack(self, &accounts) } } // V1 compatibility: HasCompressionInfo trait (wraps non-Option compression_info) - impl light_sdk::interface::HasCompressionInfo for #struct_name { - fn compression_info(&self) -> std::result::Result<&light_sdk::interface::CompressionInfo, solana_program_error::ProgramError> { + impl #on_chain_crate::HasCompressionInfo for #struct_name { + fn compression_info(&self) -> std::result::Result<&#on_chain_crate::CompressionInfo, #on_chain_crate::LightSdkTypesError> { Ok(&self.compression_info) } - fn compression_info_mut(&mut self) -> std::result::Result<&mut light_sdk::interface::CompressionInfo, solana_program_error::ProgramError> { + fn compression_info_mut(&mut self) -> std::result::Result<&mut #on_chain_crate::CompressionInfo, #on_chain_crate::LightSdkTypesError> { Ok(&mut self.compression_info) } - fn compression_info_mut_opt(&mut self) -> &mut Option { + fn compression_info_mut_opt(&mut self) -> &mut Option<#on_chain_crate::CompressionInfo> { // V2 types use non-Option CompressionInfo, so this can't return a reference // This method is only used by V1 code paths that expect Option panic!("compression_info_mut_opt not supported for LightAccount types (use compression_info_mut instead)") } - fn set_compression_info_none(&mut self) -> std::result::Result<(), solana_program_error::ProgramError> { + fn set_compression_info_none(&mut self) -> std::result::Result<(), #on_chain_crate::LightSdkTypesError> { // V2 types use non-Option CompressionInfo // Setting to "compressed" state is the equivalent of "None" for V1 - self.compression_info = light_sdk::compressible::CompressionInfo::compressed(); + self.compression_info = #on_chain_crate::CompressionInfo::compressed(); Ok(()) } } // V1 compatibility: Size trait - impl light_sdk::account::Size for #struct_name { + impl #on_chain_crate::Size for #struct_name { #[inline] - fn size(&self) -> std::result::Result { - Ok(::INIT_SPACE) + fn size(&self) -> std::result::Result { + Ok(::INIT_SPACE) } } // V1 compatibility: CompressAs trait - impl light_sdk::interface::CompressAs for #struct_name { + impl #on_chain_crate::CompressAs for #struct_name { type Output = Self; fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { @@ -372,8 +446,8 @@ fn generate_light_account_impl(input: &DeriveInput) -> Result { } // V1 compatibility: CompressedInitSpace trait - impl light_sdk::interface::CompressedInitSpace for #struct_name { - const COMPRESSED_INIT_SPACE: usize = ::INIT_SPACE; + impl #on_chain_crate::CompressedInitSpace for #struct_name { + const COMPRESSED_INIT_SPACE: usize = ::INIT_SPACE; } }; @@ -386,7 +460,10 @@ fn generate_packed_struct( packed_struct_name: &Ident, fields: &Punctuated, has_pubkey_fields: bool, + framework: Framework, ) -> Result { + let serde_derives = framework.serde_derives(); + if !has_pubkey_fields { // No Pubkey fields - Packed is just a type alias (but still excludes compression_info) // We need a minimal struct that just holds non-pubkey fields @@ -402,7 +479,7 @@ fn generate_packed_struct( if non_compression_fields.is_empty() { // Only compression_info field - create empty struct return Ok(quote! { - #[derive(Debug, Clone, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + #[derive(Debug, Clone, #serde_derives)] pub struct #packed_struct_name; }); } @@ -415,7 +492,7 @@ fn generate_packed_struct( }); return Ok(quote! { - #[derive(Debug, Clone, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + #[derive(Debug, Clone, #serde_derives)] pub struct #packed_struct_name { #(#packed_fields,)* } @@ -442,7 +519,7 @@ fn generate_packed_struct( }); Ok(quote! { - #[derive(Debug, Clone, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + #[derive(Debug, Clone, #serde_derives)] pub struct #packed_struct_name { #(#packed_fields,)* } @@ -454,6 +531,7 @@ fn generate_pack_body( packed_struct_name: &Ident, fields: &Punctuated, has_pubkey_fields: bool, + framework: Framework, ) -> Result { let pack_assignments: Vec<_> = fields .iter() @@ -468,7 +546,15 @@ fn generate_pack_body( let field_type = &field.ty; Some(if is_pubkey_type(field_type) { - quote! { #field_name: accounts.insert_or_get_read_only(self.#field_name) } + // Anchor Pubkey has .to_bytes(), pinocchio Pubkey is [u8; 32] + match framework { + Framework::Anchor => { + quote! { #field_name: accounts.insert_or_get_read_only(AM::pubkey_from_bytes(self.#field_name.to_bytes())) } + } + Framework::Pinocchio => { + quote! { #field_name: accounts.insert_or_get_read_only(AM::pubkey_from_bytes(self.#field_name)) } + } + } } else if is_copy_type(field_type) { quote! { #field_name: self.#field_name } } else { @@ -492,12 +578,15 @@ fn generate_pack_body( } /// Generates the unpack method body. +/// Uses framework-specific paths for on-chain code. fn generate_unpack_body( struct_name: &Ident, fields: &Punctuated, has_pubkey_fields: bool, + framework: Framework, ) -> Result { let struct_name_str = struct_name.to_string(); + let on_chain_crate = framework.on_chain_crate(); let unpack_assignments: Vec<_> = fields .iter() @@ -508,18 +597,24 @@ fn generate_unpack_body( // compression_info gets canonical value if field_name == "compression_info" { return Some(quote! { - #field_name: light_sdk::compressible::CompressionInfo::compressed() + #field_name: #on_chain_crate::CompressionInfo::compressed() }); } Some(if is_pubkey_type(field_type) { let error_msg = format!("{}: {}", struct_name_str, field_name); + // For Anchor: convert [u8; 32] to solana_pubkey::Pubkey + // For Pinocchio: Pubkey is [u8; 32], so use key() directly + let key_conversion = match framework { + Framework::Anchor => quote! { solana_pubkey::Pubkey::from(account.key()) }, + Framework::Pinocchio => quote! { account.key() }, + }; quote! { #field_name: { let account = accounts .get_u8(packed.#field_name, #error_msg) - .map_err(|_| solana_program_error::ProgramError::InvalidAccountData)?; - solana_pubkey::Pubkey::from(account.key()) + .map_err(|_| #on_chain_crate::LightSdkTypesError::InvalidInstructionData)?; + #key_conversion } } } else if !has_pubkey_fields { @@ -549,6 +644,7 @@ fn generate_unpack_body( fn generate_compress_as_assignments( fields: &Punctuated, compress_as_fields: &Option, + _framework: Framework, ) -> TokenStream { let Some(overrides) = compress_as_fields else { return quote! {}; @@ -588,12 +684,15 @@ fn generate_compress_as_assignments( fn generate_compress_as_impl_body( fields: &Punctuated, compress_as_fields: &Option, + framework: Framework, ) -> TokenStream { + let on_chain_crate = framework.on_chain_crate(); + let Some(overrides) = compress_as_fields else { // No overrides - clone and set compression_info to Compressed return quote! { let mut result = self.clone(); - result.compression_info = light_sdk::compressible::CompressionInfo::compressed(); + result.compression_info = #on_chain_crate::CompressionInfo::compressed(); std::borrow::Cow::Owned(result) }; }; @@ -628,14 +727,14 @@ fn generate_compress_as_impl_body( // No field overrides - clone and set compression_info to Compressed quote! { let mut result = self.clone(); - result.compression_info = light_sdk::compressible::CompressionInfo::compressed(); + result.compression_info = #on_chain_crate::CompressionInfo::compressed(); std::borrow::Cow::Owned(result) } } else { // Clone, set compression_info to Compressed, and apply overrides quote! { let mut result = self.clone(); - result.compression_info = light_sdk::compressible::CompressionInfo::compressed(); + result.compression_info = #on_chain_crate::CompressionInfo::compressed(); #(#assignments)* std::borrow::Cow::Owned(result) } @@ -683,7 +782,7 @@ mod tests { // Should contain unified LightAccount implementation assert!( - output.contains("impl light_sdk :: interface :: LightAccount for UserRecord"), + output.contains("impl light_account :: LightAccount for UserRecord"), "Should implement LightAccount trait" ); diff --git a/sdk-libs/macros/src/light_pdas/account/traits.rs b/sdk-libs/macros/src/light_pdas/account/traits.rs index 674321c9d1..71ece1c0e4 100644 --- a/sdk-libs/macros/src/light_pdas/account/traits.rs +++ b/sdk-libs/macros/src/light_pdas/account/traits.rs @@ -3,10 +3,10 @@ use darling::FromMeta; use proc_macro2::TokenStream; use quote::quote; -use syn::{punctuated::Punctuated, DeriveInput, Expr, Field, Ident, ItemStruct, Result, Token}; +use syn::{punctuated::Punctuated, Expr, Field, Ident, ItemStruct, Result, Token}; use super::{ - utils::{extract_fields_from_derive_input, extract_fields_from_item_struct, is_copy_type}, + utils::{extract_fields_from_item_struct, is_copy_type}, validation::validate_compression_info_field, }; @@ -69,13 +69,13 @@ fn generate_has_compression_info_impl( compression_info_first: bool, ) -> TokenStream { quote! { - impl light_sdk::interface::CompressionInfoField for #struct_name { + impl light_account::CompressionInfoField for #struct_name { const COMPRESSION_INFO_FIRST: bool = #compression_info_first; - fn compression_info_field(&self) -> &Option { + fn compression_info_field(&self) -> &Option { &self.compression_info } - fn compression_info_field_mut(&mut self) -> &mut Option { + fn compression_info_field_mut(&mut self) -> &mut Option { &mut self.compression_info } } @@ -135,7 +135,7 @@ fn generate_compress_as_impl( field_assignments: &[TokenStream], ) -> TokenStream { quote! { - impl light_sdk::interface::CompressAs for #struct_name { + impl light_account::CompressAs for #struct_name { type Output = Self; fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { @@ -148,34 +148,6 @@ fn generate_compress_as_impl( } } -/// Generates the Size trait implementation. -/// Uses max(INIT_SPACE, serialized_len) to ensure enough space while handling edge cases. -fn generate_size_impl(struct_name: &Ident) -> TokenStream { - quote! { - impl light_sdk::account::Size for #struct_name { - #[inline] - fn size(&self) -> std::result::Result { - // Use Anchor's compile-time INIT_SPACE as the baseline. - // Fall back to serialized length if it's somehow larger (edge case safety). - let init_space = ::INIT_SPACE; - let serialized_len = self.try_to_vec() - .map_err(|_| solana_program_error::ProgramError::BorshIoError("serialization failed".to_string()))? - .len(); - Ok(core::cmp::max(init_space, serialized_len)) - } - } - } -} - -/// Generates the CompressedInitSpace trait implementation -fn generate_compressed_init_space_impl(struct_name: &Ident) -> TokenStream { - quote! { - impl light_sdk::interface::CompressedInitSpace for #struct_name { - const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; - } - } -} - pub fn derive_compress_as(input: ItemStruct) -> Result { let struct_name = &input.ident; let fields = extract_fields_from_item_struct(&input)?; @@ -207,44 +179,3 @@ pub fn derive_has_compression_info(input: syn::ItemStruct) -> Result Result { - let struct_name = &input.ident; - let fields = extract_fields_from_derive_input(&input)?; - - // Extract compress_as attribute using darling - let compress_as_attr = input - .attrs - .iter() - .find(|attr| attr.path().is_ident("compress_as")); - - let compress_as_fields = if let Some(attr) = compress_as_attr { - let parsed = CompressAsFields::from_meta(&attr.meta) - .map_err(|e| syn::Error::new_spanned(attr, e.to_string()))?; - Some(parsed) - } else { - None - }; - - // Validate compression_info field exists and get its position - let compression_info_first = validate_compression_info_field(fields, struct_name)?; - - // Generate all trait implementations using helper functions - let has_compression_info_impl = - generate_has_compression_info_impl(struct_name, compression_info_first); - - let field_assignments = generate_compress_as_field_assignments(fields, &compress_as_fields); - let compress_as_impl = generate_compress_as_impl(struct_name, &field_assignments); - - let size_impl = generate_size_impl(struct_name); - - let compressed_init_space_impl = generate_compressed_init_space_impl(struct_name); - - // Combine all implementations - Ok(quote! { - #has_compression_info_impl - #compress_as_impl - #size_impl - #compressed_init_space_impl - }) -} diff --git a/sdk-libs/macros/src/light_pdas/accounts/builder.rs b/sdk-libs/macros/src/light_pdas/accounts/builder.rs index c272442707..0e68f22e01 100644 --- a/sdk-libs/macros/src/light_pdas/accounts/builder.rs +++ b/sdk-libs/macros/src/light_pdas/accounts/builder.rs @@ -144,24 +144,24 @@ impl LightAccountsBuilder { Ok(quote! { #[automatically_derived] - impl #impl_generics light_sdk::interface::LightPreInit<'info, ()> for #struct_name #ty_generics #where_clause { + impl #impl_generics light_account::LightPreInit, ()> for #struct_name #ty_generics #where_clause { fn light_pre_init( &mut self, _remaining: &[solana_account_info::AccountInfo<'info>], _params: &(), - ) -> std::result::Result { + ) -> std::result::Result { Ok(false) } } #[automatically_derived] - impl #impl_generics light_sdk::interface::LightFinalize<'info, ()> for #struct_name #ty_generics #where_clause { + impl #impl_generics light_account::LightFinalize, ()> 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> { + ) -> std::result::Result<(), light_account::LightSdkTypesError> { Ok(()) } } @@ -299,14 +299,14 @@ impl LightAccountsBuilder { let compression_config = &self.infra.compression_config; Ok(quote! { - let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new_with_config( + let cpi_accounts = light_account::CpiAccounts::new_with_config( &self.#fee_payer, _remaining, - light_sdk::cpi::CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), + light_account::CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), ); - let compression_config_data = light_sdk::interface::LightConfig::load_checked( + let compression_config_data = light_account::LightConfig::load_checked( &self.#compression_config, - &crate::ID, + &crate::LIGHT_CPI_SIGNER.program_id, )?; let mut all_new_address_params = Vec::with_capacity(#rentfree_count as usize); @@ -316,7 +316,7 @@ impl LightAccountsBuilder { // Reimburse fee payer for rent paid during PDA creation #rent_reimbursement - light_token::compressible::invoke_write_pdas_to_cpi_context( + light_account::invoke_write_pdas_to_cpi_context( crate::LIGHT_CPI_SIGNER, #proof_access.proof.clone(), &all_new_address_params, @@ -342,16 +342,16 @@ impl LightAccountsBuilder { let compression_config = &self.infra.compression_config; Ok(quote! { - use light_sdk::cpi::{LightCpiInstruction, InvokeLightSystemProgram}; + use light_account::InvokeLightSystemProgram; - let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( + let cpi_accounts = light_account::CpiAccounts::new( &self.#fee_payer, _remaining, crate::LIGHT_CPI_SIGNER, ); - let compression_config_data = light_sdk::interface::LightConfig::load_checked( + let compression_config_data = light_account::LightConfig::load_checked( &self.#compression_config, - &crate::ID, + &crate::LIGHT_CPI_SIGNER.program_id, )?; let mut all_new_address_params = Vec::with_capacity(#rentfree_count as usize); @@ -361,13 +361,22 @@ impl LightAccountsBuilder { // Reimburse fee payer for rent paid during PDA creation #rent_reimbursement - light_sdk::cpi::v2::LightSystemProgramCpi::new_cpi( - crate::LIGHT_CPI_SIGNER, - #proof_access.proof.clone(), - ) - .with_new_addresses(&all_new_address_params) - .with_account_infos(&all_compressed_infos) - .invoke(cpi_accounts)?; + let instruction_data = light_compressed_account::instruction_data::with_account_info::InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: crate::LIGHT_CPI_SIGNER.bump, + invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: false, + with_transaction_hash: false, + cpi_context: light_compressed_account::instruction_data::cpi_context::CompressedCpiContext::default(), + proof: #proof_access.proof.clone().0, + new_address_params: all_new_address_params, + account_infos: all_compressed_infos, + read_only_addresses: vec![], + read_only_accounts: vec![], + }; + instruction_data.invoke(cpi_accounts)?; }) } @@ -383,10 +392,10 @@ impl LightAccountsBuilder { let fee_payer = &self.infra.fee_payer; Ok(quote! { - let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new_with_config( + let cpi_accounts = light_account::CpiAccounts::new_with_config( &self.#fee_payer, _remaining, - light_sdk::cpi::CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), + light_account::CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), ); #mint_invocation @@ -405,12 +414,12 @@ impl LightAccountsBuilder { Ok(quote! { #[automatically_derived] - impl #impl_generics light_sdk::interface::LightPreInit<'info, #params_type> for #struct_name #ty_generics #where_clause { + impl #impl_generics light_account::LightPreInit, #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 { + ) -> std::result::Result { use anchor_lang::ToAccountInfo; #body } @@ -430,13 +439,13 @@ impl LightAccountsBuilder { Ok(quote! { #[automatically_derived] - impl #impl_generics light_sdk::interface::LightFinalize<'info, #params_type> for #struct_name #ty_generics #where_clause { + impl #impl_generics light_account::LightFinalize, #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> { + ) -> std::result::Result<(), light_account::LightSdkTypesError> { use anchor_lang::ToAccountInfo; #body } diff --git a/sdk-libs/macros/src/light_pdas/accounts/light_account.rs b/sdk-libs/macros/src/light_pdas/accounts/light_account.rs index f9f63ccf38..5a1becdd29 100644 --- a/sdk-libs/macros/src/light_pdas/accounts/light_account.rs +++ b/sdk-libs/macros/src/light_pdas/accounts/light_account.rs @@ -734,8 +734,6 @@ fn build_pda_field( /// - `mint::freeze_authority` -> `freeze_authority` /// - `mint::authority_seeds` -> `authority_seeds` /// - `mint::authority_bump` -> `authority_bump` -/// - `mint::rent_payment` -> `rent_payment` -/// - `mint::write_top_up` -> `write_top_up` /// - `mint::name` -> `name` /// - `mint::symbol` -> `symbol` /// - `mint::uri` -> `uri` @@ -758,8 +756,6 @@ fn build_mint_field( let mut authority_seeds: Option = None; let mut mint_bump: Option = None; let mut authority_bump: Option = None; - let mut rent_payment: Option = None; - let mut write_top_up: Option = None; // Metadata fields let mut name: Option = None; @@ -783,8 +779,6 @@ fn build_mint_field( } "authority_seeds" => authority_seeds = Some(kv.value.clone()), "authority_bump" => authority_bump = Some(kv.value.clone()), - "rent_payment" => rent_payment = Some(kv.value.clone()), - "write_top_up" => write_top_up = Some(kv.value.clone()), // Metadata fields "name" => name = Some(kv.value.clone()), @@ -845,16 +839,10 @@ fn build_mint_field( )?; // Always fetch from CreateAccountsProof - depends on whether proof is direct arg or nested - let (address_tree_info, output_tree) = if let Some(proof_ident) = direct_proof_arg { - ( - syn::parse_quote!(#proof_ident.address_tree_info), - syn::parse_quote!(#proof_ident.output_state_tree_index), - ) + let address_tree_info = if let Some(proof_ident) = direct_proof_arg { + syn::parse_quote!(#proof_ident.address_tree_info) } else { - ( - syn::parse_quote!(params.create_accounts_proof.address_tree_info), - syn::parse_quote!(params.create_accounts_proof.output_state_tree_index), - ) + syn::parse_quote!(params.create_accounts_proof.address_tree_info) }; Ok(LightMintField { @@ -863,14 +851,11 @@ fn build_mint_field( authority, decimals, address_tree_info, - output_tree, freeze_authority, mint_seeds, mint_bump, authority_seeds, authority_bump, - rent_payment, - write_top_up, name, symbol, uri, @@ -1817,21 +1802,6 @@ mod tests { "address_tree_info should access .address_tree_info field, got: {}", addr_tree_str ); - - // Verify default output_tree uses the direct proof identifier - // Should be: create_proof.output_state_tree_index - let output_tree = &mint.output_tree; - let output_tree_str = quote::quote!(#output_tree).to_string(); - assert!( - output_tree_str.contains("create_proof"), - "output_tree should reference 'create_proof', got: {}", - output_tree_str - ); - assert!( - output_tree_str.contains("output_state_tree_index"), - "output_tree should access .output_state_tree_index field, got: {}", - output_tree_str - ); } _ => panic!("Expected Mint field"), } diff --git a/sdk-libs/macros/src/light_pdas/accounts/mint.rs b/sdk-libs/macros/src/light_pdas/accounts/mint.rs index d04f7618a4..ecd3f2e817 100644 --- a/sdk-libs/macros/src/light_pdas/accounts/mint.rs +++ b/sdk-libs/macros/src/light_pdas/accounts/mint.rs @@ -34,8 +34,6 @@ pub(crate) struct LightMintField { pub decimals: Expr, /// Address tree info expression (auto-fetched from CreateAccountsProof) pub address_tree_info: Expr, - /// Output state tree index expression (auto-fetched from CreateAccountsProof) - pub output_tree: Expr, /// Optional freeze authority pub freeze_authority: Option, /// Signer seeds for the mint_signer PDA (required, WITHOUT bump - bump is auto-derived or provided via mint_bump) @@ -46,10 +44,6 @@ pub(crate) struct LightMintField { pub authority_seeds: Option, /// Optional bump for authority_seeds. If None, auto-derived using find_program_address. pub authority_bump: 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, // Metadata extension fields /// Token name for TokenMetadata extension pub name: Option, @@ -67,11 +61,6 @@ pub(crate) struct LightMintField { // Code Generation // ============================================================================ -/// Quote an optional expression, using default if None. -fn quote_option_or(opt: &Option, default: TokenStream) -> TokenStream { - opt.as_ref().map(|e| quote! { #e }).unwrap_or(default) -} - /// Resolve optional field name to TokenStream, using default if None. fn resolve_field_name(field: &Option, default: &str) -> TokenStream { field.as_ref().map(|f| quote! { #f }).unwrap_or_else(|| { @@ -205,14 +194,12 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { let freeze_authority = mint .freeze_authority .as_ref() - .map(|f| quote! { Some(*self.#f.to_account_info().key) }) + .map(|f| quote! { Some(self.#f.to_account_info().key.to_bytes()) }) .unwrap_or_else(|| quote! { None }); let mint_seeds = &mint.mint_seeds; let authority_seeds = &mint.authority_seeds; let idx_ident = format_ident!("__mint_param_{}", idx); - let pda_ident = format_ident!("__mint_pda_{}", idx); - let bump_ident = format_ident!("__mint_bump_{}", idx); let signer_key_ident = format_ident!("__mint_signer_key_{}", idx); let mint_seeds_ident = format_ident!("__mint_seeds_{}", idx); let mint_seeds_with_bump_ident = format_ident!("__mint_seeds_with_bump_{}", idx); @@ -231,7 +218,7 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { // Auto-derive bump from mint_seeds quote! { let #mint_signer_bump_ident: u8 = { - let (_, bump) = solana_pubkey::Pubkey::find_program_address(#mint_seeds_ident, &crate::ID); + let (_, bump) = solana_pubkey::Pubkey::find_program_address(#mint_seeds_ident, &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id)); bump }; } @@ -248,7 +235,7 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { quote! { let #authority_bump_ident: u8 = { let base_seeds: &[&[u8]] = #seeds; - let (_, bump) = solana_pubkey::Pubkey::find_program_address(base_seeds, &crate::ID); + let (_, bump) = solana_pubkey::Pubkey::find_program_address(base_seeds, &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id)); bump }; } @@ -285,8 +272,8 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { .unwrap_or_else(|| quote! { None }); quote! { - let #token_metadata_ident: Option = Some( - light_token::TokenMetadataInstructionData { + let #token_metadata_ident: Option = Some( + light_account::TokenMetadataInstructionData { update_authority: #update_authority_expr, name: #name_expr, symbol: #symbol_expr, @@ -297,14 +284,13 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { } } else { quote! { - let #token_metadata_ident: Option = None; + let #token_metadata_ident: Option = None; } }; quote! { - // Mint #idx: derive PDA and build params - let #signer_key_ident = *self.#mint_signer.to_account_info().key; - let (#pda_ident, #bump_ident) = light_token::instruction::find_mint_address(&#signer_key_ident); + // Mint #idx: build params (mint and compression_address derived internally) + let #signer_key_ident: [u8; 32] = self.#mint_signer.to_account_info().key.to_bytes(); // Bind base mint_seeds (WITHOUT bump) and derive/get bump let #mint_seeds_ident: &[&[u8]] = #mint_seeds; @@ -319,13 +305,10 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { let __tree_info = &#address_tree_info; - let #idx_ident = light_token::instruction::SingleMintParams { + let #idx_ident = light_account::SingleMintParams { decimals: #decimals, - address_merkle_tree_root_index: __tree_info.root_index, - mint_authority: *self.#authority.to_account_info().key, - compression_address: #pda_ident.to_bytes(), - mint: #pda_ident, - bump: #bump_ident, + mint_authority: self.#authority.to_account_info().key.to_bytes(), + mint_bump: None, // derived internally from mint_seed_pubkey freeze_authority: #freeze_authority, mint_seed_pubkey: #signer_key_ident, authority_seeds: #authority_seeds_with_bump_ident.as_deref(), @@ -362,11 +345,6 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { }) .collect(); - // Get shared params from first mint (all mints share same params for now) - let rent_payment = quote_option_or(&mints[0].rent_payment, quote! { 16u8 }); - let write_top_up = quote_option_or(&mints[0].write_top_up, quote! { 766u32 }); - let output_tree = &mints[0].output_tree; - // Authority signer check for mints without authority_seeds let authority_signer_checks: Vec = mints .iter() @@ -385,15 +363,11 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { { #output_tree_setup - // Extract proof from instruction params - let __proof: light_token::CompressedProof = #proof_access.proof.0.clone() - .expect("proof is required for mint creation"); - // Build SingleMintParams for each mint #(#mint_params_builds)* // Array of mint params - let __mint_params: [light_token::instruction::SingleMintParams<'_>; #mint_count] = [ + let __mint_params: [light_account::SingleMintParams<'_>; #mint_count] = [ #(#param_idents),* ]; @@ -407,38 +381,24 @@ fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { #(#mint_account_exprs),* ]; - // Get tree indices from proof - let __tree_info = &#proof_access.address_tree_info; - let __output_queue_index: u8 = #output_tree; - let __state_tree_index: u8 = #proof_access.state_tree_index - .ok_or(anchor_lang::prelude::ProgramError::InvalidArgument)?; - let __address_tree_index: u8 = __tree_info.address_merkle_tree_pubkey_index; - // Check authority signers for mints without authority_seeds #(#authority_signer_checks)* - // Build params and invoke CreateMintsCpi via helper - light_token::compressible::invoke_create_mints( - &__mint_seed_accounts, - &__mint_accounts, - light_token::instruction::CreateMintsParams { - mints: &__mint_params, - proof: __proof, - rent_payment: #rent_payment, - write_top_up: #write_top_up, - cpi_context_offset: #cpi_context_offset, - output_queue_index: __output_queue_index, - address_tree_index: __address_tree_index, - state_tree_index: __state_tree_index, - }, - light_token::compressible::CreateMintsInfraAccounts { - fee_payer: self.#fee_payer.to_account_info(), - compressible_config: self.#light_token_config.to_account_info(), - rent_sponsor: self.#light_token_rent_sponsor.to_account_info(), - cpi_authority: self.#light_token_cpi_authority.to_account_info(), + // Build CreateMints struct and invoke + light_account::CreateMints { + mints: &__mint_params, + proof_data: &#proof_access, + mint_seed_accounts: &__mint_seed_accounts, + mint_accounts: &__mint_accounts, + static_accounts: light_account::CreateMintsStaticAccounts { + fee_payer: &self.#fee_payer.to_account_info(), + compressible_config: &self.#light_token_config.to_account_info(), + rent_sponsor: &self.#light_token_rent_sponsor.to_account_info(), + cpi_authority: &self.#light_token_cpi_authority.to_account_info(), }, - &cpi_accounts, - )?; + cpi_context_offset: #cpi_context_offset, + } + .invoke(&cpi_accounts)?; } } } diff --git a/sdk-libs/macros/src/light_pdas/accounts/pda.rs b/sdk-libs/macros/src/light_pdas/accounts/pda.rs index aefb103106..6523bafd5a 100644 --- a/sdk-libs/macros/src/light_pdas/accounts/pda.rs +++ b/sdk-libs/macros/src/light_pdas/accounts/pda.rs @@ -55,7 +55,7 @@ impl<'a> PdaBlockBuilder<'a> { quote! { let #account_info = self.#field_name.to_account_info(); - let #account_key = *#account_info.key; + let #account_key = #account_info.key.to_bytes(); } } @@ -69,13 +69,12 @@ impl<'a> PdaBlockBuilder<'a> { let address_tree_pubkey = &self.idents.address_tree_pubkey; quote! { - let #address_tree_pubkey: solana_pubkey::Pubkey = { - use light_sdk::light_account_checks::AccountInfoTrait; + let #address_tree_pubkey: [u8; 32] = { // Explicit type annotation ensures clear error if wrong type is provided. - let tree_info: &::light_sdk::sdk_types::PackedAddressTreeInfo = &#addr_tree_info; - cpi_accounts - .get_tree_account_info(tree_info.address_merkle_tree_pubkey_index as usize)? - .pubkey() + let tree_info: &light_account::PackedAddressTreeInfo = &#addr_tree_info; + let __tree_account = cpi_accounts + .get_tree_account_info(tree_info.address_merkle_tree_pubkey_index as usize)?; + light_account::AccountInfoTrait::key(__tree_account) }; } } @@ -90,13 +89,14 @@ impl<'a> PdaBlockBuilder<'a> { let account_guard = format_ident!("{}_guard", ident); quote! { { - let current_slot = anchor_lang::solana_program::sysvar::clock::Clock::get()?.slot; + let current_slot = anchor_lang::solana_program::sysvar::clock::Clock::get() + .map_err(|_| light_account::LightSdkTypesError::ConstraintViolation)?.slot; let mut #account_guard = self.#ident.load_init() - .map_err(|_| solana_program_error::ProgramError::InvalidAccountData)?; + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)?; let #account_data = &mut *#account_guard; // For zero-copy Pod accounts, set compression_info directly #account_data.compression_info = - light_sdk::compressible::CompressionInfo::new_from_config( + light_account::CompressionInfo::new_from_config( &compression_config_data, current_slot, ); @@ -105,9 +105,10 @@ impl<'a> PdaBlockBuilder<'a> { } else if self.field.is_boxed { quote! { { - use light_sdk::interface::LightAccount; + use light_account::LightAccount; use anchor_lang::AnchorSerialize; - let current_slot = anchor_lang::solana_program::sysvar::clock::Clock::get()?.slot; + let current_slot = anchor_lang::solana_program::sysvar::clock::Clock::get() + .map_err(|_| light_account::LightSdkTypesError::ConstraintViolation)?.slot; // Get account info BEFORE mutable borrow let account_info = self.#ident.to_account_info(); // Scope the mutable borrow @@ -119,17 +120,18 @@ impl<'a> PdaBlockBuilder<'a> { // Now serialize - the mutable borrow above is released let mut data = account_info .try_borrow_mut_data() - .map_err(|_| light_sdk::error::LightSdkError::ConstraintViolation)?; + .map_err(|_| light_account::LightSdkTypesError::ConstraintViolation)?; self.#ident.serialize(&mut &mut data[8..]) - .map_err(|_| light_sdk::error::LightSdkError::ConstraintViolation)?; + .map_err(|_| light_account::LightSdkTypesError::ConstraintViolation)?; } } } else { quote! { { - use light_sdk::interface::LightAccount; + use light_account::LightAccount; use anchor_lang::AnchorSerialize; - let current_slot = anchor_lang::solana_program::sysvar::clock::Clock::get()?.slot; + let current_slot = anchor_lang::solana_program::sysvar::clock::Clock::get() + .map_err(|_| light_account::LightSdkTypesError::ConstraintViolation)?.slot; // Get account info BEFORE mutable borrow let account_info = self.#ident.to_account_info(); // Scope the mutable borrow @@ -141,9 +143,9 @@ impl<'a> PdaBlockBuilder<'a> { // Now serialize - the mutable borrow above is released let mut data = account_info .try_borrow_mut_data() - .map_err(|_| light_sdk::error::LightSdkError::ConstraintViolation)?; + .map_err(|_| light_account::LightSdkTypesError::ConstraintViolation)?; self.#ident.serialize(&mut &mut data[8..]) - .map_err(|_| light_sdk::error::LightSdkError::ConstraintViolation)?; + .map_err(|_| light_account::LightSdkTypesError::ConstraintViolation)?; } } } @@ -168,15 +170,15 @@ impl<'a> PdaBlockBuilder<'a> { quote! { { // Explicit type annotation for tree_info - let tree_info: &::light_sdk::sdk_types::PackedAddressTreeInfo = &#addr_tree_info; + let tree_info: &light_account::PackedAddressTreeInfo = &#addr_tree_info; - ::light_sdk::interface::prepare_compressed_account_on_init( + ::light_account::prepare_compressed_account_on_init( &#account_key, &#address_tree_pubkey, tree_info, #output_tree, #idx, - &crate::ID, + &crate::LIGHT_CPI_SIGNER.program_id, &mut all_new_address_params, &mut all_compressed_infos, )?; @@ -248,11 +250,16 @@ pub(super) fn generate_rent_reimbursement_block( let __created_accounts: [solana_account_info::AccountInfo<'info>; #count] = [ #(#account_info_exprs),* ]; - ::light_sdk::interface::reimburse_rent( + let __rent_sponsor_bump_byte = [compression_config_data.rent_sponsor_bump]; + let __rent_sponsor_seeds: &[&[u8]] = &[ + light_account::RENT_SPONSOR_SEED, + &__rent_sponsor_bump_byte, + ]; + ::light_account::reimburse_rent( &__created_accounts, &self.#fee_payer.to_account_info(), &self.#rent_sponsor.to_account_info(), - &crate::ID, + __rent_sponsor_seeds, )?; } } diff --git a/sdk-libs/macros/src/light_pdas/accounts/token.rs b/sdk-libs/macros/src/light_pdas/accounts/token.rs index a8b8e770ef..15918f317d 100644 --- a/sdk-libs/macros/src/light_pdas/accounts/token.rs +++ b/sdk-libs/macros/src/light_pdas/accounts/token.rs @@ -95,7 +95,7 @@ pub(super) fn generate_token_account_cpi( if token_seeds.is_empty() { quote! { let __bump: u8 = { - let (_, bump) = solana_pubkey::Pubkey::find_program_address(&[], &crate::ID); + let (_, bump) = solana_pubkey::Pubkey::find_program_address(&[], &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id)); bump }; } @@ -103,7 +103,7 @@ pub(super) fn generate_token_account_cpi( quote! { let __bump: u8 = { let seeds: &[&[u8]] = &[#(#seed_refs),*]; - let (_, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &crate::ID); + let (_, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id)); bump }; } @@ -117,25 +117,24 @@ pub(super) fn generate_token_account_cpi( quote! { &[#(#seed_refs,)* &__bump_slice[..]] } }; - // Get mint and owner from field or derive from context - // mint is used as AccountInfo for CPI - let mint_account_info = field + // Get mint binding from field or default + let mint_binding = field .mint .as_ref() - .map(|m| quote! { self.#m.to_account_info() }) - .unwrap_or_else(|| quote! { self.mint.to_account_info() }); + .map(|m| quote! { let __mint_info = self.#m.to_account_info(); }) + .unwrap_or_else(|| quote! { let __mint_info = self.mint.to_account_info(); }); - // owner is a Pubkey - the owner of the token account + // owner is [u8; 32] - the owner of the token account let owner_expr = field .owner .as_ref() - .map(|o| quote! { *self.#o.to_account_info().key }) - .unwrap_or_else(|| quote! { *self.fee_payer.to_account_info().key }); + .map(|o| quote! { self.#o.to_account_info().key.to_bytes() }) + .unwrap_or_else(|| quote! { self.fee_payer.to_account_info().key.to_bytes() }); Some(quote! { // Create token account: #field_ident { - use light_token::instruction::CreateTokenAccountCpi; + use light_account::CreateTokenAccountCpi; // Bind seeds to local variables to extend temporary lifetimes #(#seed_bindings)* @@ -145,17 +144,24 @@ pub(super) fn generate_token_account_cpi( let __bump_slice: [u8; 1] = [__bump]; let __token_account_seeds: &[&[u8]] = #seeds_array_expr; + // Bind account infos to local variables so we can pass references + let __payer_info = self.#fee_payer.to_account_info(); + let __account_info = self.#field_ident.to_account_info(); + #mint_binding + let __config_info = self.#light_token_config.to_account_info(); + let __sponsor_info = self.#light_token_rent_sponsor.to_account_info(); + CreateTokenAccountCpi { - payer: self.#fee_payer.to_account_info(), - account: self.#field_ident.to_account_info(), - mint: #mint_account_info, + payer: &__payer_info, + account: &__account_info, + mint: &__mint_info, owner: #owner_expr, } .rent_free( - self.#light_token_config.to_account_info(), - self.#light_token_rent_sponsor.to_account_info(), - __system_program.clone(), - &crate::ID, + &__config_info, + &__sponsor_info, + &__system_program, + &crate::LIGHT_CPI_SIGNER.program_id, ) .invoke_signed(__token_account_seeds)?; } @@ -187,9 +193,9 @@ pub(super) fn generate_ata_cpi(field: &AtaField, infra: &InfraRefs) -> Option( + &self.#owner.to_account_info().key.to_bytes(), + &self.#mint.to_account_info().key.to_bytes(), ); bump } @@ -199,20 +205,28 @@ pub(super) fn generate_ata_cpi(field: &AtaField, infra: &InfraRefs) -> Option TokenStream { + let seeds_struct = self.generate_seeds_struct_pinocchio(); + let packed_seeds_struct = self.generate_packed_seeds_struct_pinocchio(); + let variant_struct = self.generate_variant_struct_pinocchio(); + let packed_variant_struct = self.generate_packed_variant_struct_pinocchio(); + let light_account_variant_impl = self.generate_light_account_variant_impl_pinocchio(); + let packed_light_account_variant_impl = + self.generate_packed_light_account_variant_impl_pinocchio(); + let pack_impl = self.generate_pack_impl_pinocchio(); + + quote! { + #seeds_struct + #packed_seeds_struct + #variant_struct + #packed_variant_struct + #light_account_variant_impl + #packed_light_account_variant_impl + #pack_impl + } + } + + // ========================================================================= + // PINOCCHIO GENERATION METHODS + // ========================================================================= + + fn generate_seeds_struct_pinocchio(&self) -> TokenStream { + let struct_name = format_ident!("{}Seeds", self.variant_name); + let fields: Vec<_> = self + .seed_fields + .iter() + .map(|sf| { + let name = &sf.field_name; + let ty = if sf.is_account_seed { + quote! { [u8; 32] } + } else if sf.has_le_bytes { + quote! { u64 } + } else { + quote! { [u8; 32] } + }; + quote! { pub #name: #ty } + }) + .collect(); + + if fields.is_empty() { + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub struct #struct_name; + } + } else { + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub struct #struct_name { + #(#fields,)* + } + } + } + } + + fn generate_packed_seeds_struct_pinocchio(&self) -> TokenStream { + let struct_name = format_ident!("Packed{}Seeds", self.variant_name); + let fields: Vec<_> = self + .seed_fields + .iter() + .map(|sf| { + let name = if sf.is_account_seed { + format_ident!("{}_idx", sf.field_name) + } else { + sf.field_name.clone() + }; + let ty = &sf.packed_field_type; + quote! { pub #name: #ty } + }) + .collect(); + + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub struct #struct_name { + #(#fields,)* + pub bump: u8, + } + } + } + + fn generate_variant_struct_pinocchio(&self) -> TokenStream { + let struct_name = format_ident!("{}Variant", self.variant_name); + let seeds_struct_name = format_ident!("{}Seeds", self.variant_name); + let inner_type = &self.inner_type; + + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub struct #struct_name { + pub seeds: #seeds_struct_name, + pub data: #inner_type, + } + } + } + + fn generate_packed_variant_struct_pinocchio(&self) -> TokenStream { + let struct_name = format_ident!("Packed{}Variant", self.variant_name); + let packed_seeds_struct_name = format_ident!("Packed{}Seeds", self.variant_name); + let inner_type = &self.inner_type; + let data_type = if let Some(packed_type) = make_packed_type(inner_type) { + quote! { #packed_type } + } else { + let type_str = quote!(#inner_type).to_string().replace(' ', ""); + let packed_name = format_ident!("Packed{}", type_str); + quote! { #packed_name } + }; + + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub struct #struct_name { + pub seeds: #packed_seeds_struct_name, + pub data: #data_type, + } + } + } + + fn generate_light_account_variant_impl_pinocchio(&self) -> TokenStream { + let variant_name = format_ident!("{}Variant", self.variant_name); + let seeds_struct_name = format_ident!("{}Seeds", self.variant_name); + let packed_variant_name = format_ident!("Packed{}Variant", self.variant_name); + let inner_type = &self.inner_type; + let seed_count = self.seed_count; + + let seed_vec_items = self.generate_seed_vec_items_pinocchio(); + let seed_refs_items = self.generate_seed_refs_items(); + + quote! { + impl light_account_pinocchio::LightAccountVariantTrait<#seed_count> for #variant_name { + const PROGRAM_ID: [u8; 32] = crate::LIGHT_CPI_SIGNER.program_id; + + type Seeds = #seeds_struct_name; + type Data = #inner_type; + type Packed = #packed_variant_name; + + fn data(&self) -> &Self::Data { + &self.data + } + + fn seed_vec(&self) -> Vec> { + vec![#(#seed_vec_items),*] + } + + fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; #seed_count] { + [#(#seed_refs_items,)* bump_storage] + } + } + } + } + + fn generate_packed_light_account_variant_impl_pinocchio(&self) -> TokenStream { + let variant_name = format_ident!("{}Variant", self.variant_name); + let seeds_struct_name = format_ident!("{}Seeds", self.variant_name); + let packed_variant_name = format_ident!("Packed{}Variant", self.variant_name); + let inner_type = &self.inner_type; + let seed_count = self.seed_count; + + let unpack_seed_stmts = self.generate_unpack_seed_statements_pinocchio(); + let unpack_seed_fields = self.generate_unpack_seed_fields_pinocchio(); + let packed_seed_refs_items = self.generate_packed_seed_refs_items_pinocchio(); + + let unpack_data = quote! { + { + let packed_accounts = light_account_pinocchio::light_account_checks::packed_accounts::ProgramPackedAccounts { accounts }; + <#inner_type as light_account_pinocchio::LightAccount>::unpack(&self.data, &packed_accounts) + .map_err(|_| light_account_pinocchio::LightSdkTypesError::InvalidInstructionData)? + } + }; + + quote! { + impl light_account_pinocchio::PackedLightAccountVariantTrait<#seed_count> for #packed_variant_name { + type Unpacked = #variant_name; + + const ACCOUNT_TYPE: light_account_pinocchio::AccountType = + <#inner_type as light_account_pinocchio::LightAccount>::ACCOUNT_TYPE; + + fn bump(&self) -> u8 { + self.seeds.bump + } + + fn unpack(&self, accounts: &[AI]) -> std::result::Result { + #(#unpack_seed_stmts)* + + Ok(#variant_name { + seeds: #seeds_struct_name { + #(#unpack_seed_fields,)* + }, + data: #unpack_data, + }) + } + + fn seed_refs_with_bump<'a, AI: light_account_pinocchio::light_account_checks::AccountInfoTrait>( + &'a self, + accounts: &'a [AI], + bump_storage: &'a [u8; 1], + ) -> std::result::Result<[&'a [u8]; #seed_count], light_account_pinocchio::LightSdkTypesError> { + Ok([#(#packed_seed_refs_items,)* bump_storage]) + } + } + } + } + + fn generate_pack_impl_pinocchio(&self) -> TokenStream { + let variant_name = format_ident!("{}Variant", self.variant_name); + let packed_variant_name = format_ident!("Packed{}Variant", self.variant_name); + let packed_seeds_struct_name = format_ident!("Packed{}Seeds", self.variant_name); + let inner_type = &self.inner_type; + + let pack_seed_fields = self.generate_pack_seed_fields_pinocchio(); + + let pack_data = quote! { + <#inner_type as light_account_pinocchio::LightAccount>::pack(&self.data, accounts) + .map_err(|_| light_account_pinocchio::LightSdkTypesError::InvalidInstructionData)? + }; + + quote! { + #[cfg(not(target_os = "solana"))] + impl light_account_pinocchio::Pack for #variant_name { + type Packed = #packed_variant_name; + + fn pack( + &self, + accounts: &mut light_account_pinocchio::PackedAccounts, + ) -> std::result::Result { + use light_account_pinocchio::LightAccountVariantTrait; + let (_, bump) = self.derive_pda::(); + Ok(#packed_variant_name { + seeds: #packed_seeds_struct_name { + #(#pack_seed_fields,)* + bump, + }, + data: #pack_data, + }) + } + } + } + } + + /// Generate seed_vec items for pinocchio (uses `.to_vec()` on `[u8; 32]` instead of `.to_bytes().to_vec()`). + fn generate_seed_vec_items_pinocchio(&self) -> Vec { + self.seeds + .iter() + .map(|seed| match seed { + ClassifiedSeed::Literal(_) + | ClassifiedSeed::Constant { .. } + | ClassifiedSeed::Passthrough(_) => { + let expr = seed_to_expr(seed, self.module_path.as_deref()); + quote! { (#expr).to_vec() } + } + ClassifiedSeed::CtxRooted { account, .. } => { + quote! { self.seeds.#account.to_vec() } + } + ClassifiedSeed::DataRooted { root, expr, .. } => { + let field = extract_data_field_name(root, expr); + if is_le_bytes_expr(expr) { + quote! { self.seeds.#field.to_le_bytes().to_vec() } + } else { + quote! { self.seeds.#field.to_vec() } + } + } + ClassifiedSeed::FunctionCall { + func_expr, + args, + has_as_ref, + } => { + let rewritten = rewrite_fn_call_for_self(func_expr, args); + if *has_as_ref { + quote! { #rewritten.as_ref().to_vec() } + } else { + quote! { (#rewritten).to_vec() } + } + } + }) + .collect() + } + + fn generate_unpack_seed_statements_pinocchio(&self) -> Vec { + self.seed_fields + .iter() + .filter(|sf| sf.is_account_seed) + .map(|sf| { + let field = &sf.field_name; + let idx_field = format_ident!("{}_idx", field); + quote! { + let #field: [u8; 32] = + accounts + .get(self.seeds.#idx_field as usize) + .ok_or(light_account_pinocchio::LightSdkTypesError::NotEnoughAccountKeys)? + .key(); + } + }) + .collect() + } + + fn generate_unpack_seed_fields_pinocchio(&self) -> Vec { + self.seed_fields + .iter() + .map(|sf| { + let field = &sf.field_name; + if sf.is_account_seed { + quote! { #field } + } else if sf.has_le_bytes { + quote! { #field: u64::from_le_bytes(self.seeds.#field) } + } else { + quote! { #field: self.seeds.#field } + } + }) + .collect() + } + + fn generate_packed_seed_refs_items_pinocchio(&self) -> Vec { + self.seeds + .iter() + .map(|seed| match seed { + ClassifiedSeed::Literal(_) | ClassifiedSeed::Constant { .. } => { + let expr = seed_to_expr(seed, self.module_path.as_deref()); + quote! { #expr } + } + ClassifiedSeed::Passthrough(pass_expr) => { + if expr_contains_call(pass_expr) { + quote! { + { + panic!("seed_refs_with_bump not supported for function call seeds on packed variant."); + #[allow(unreachable_code)] + { bump_storage as &[u8] } + } + } + } else { + let expr = seed_to_expr(seed, self.module_path.as_deref()); + quote! { #expr } + } + } + ClassifiedSeed::CtxRooted { account, .. } => { + let idx_field = format_ident!("{}_idx", account); + quote! { + accounts + .get(self.seeds.#idx_field as usize) + .ok_or(light_account_pinocchio::LightSdkTypesError::InvalidInstructionData)? + .key_ref() + } + } + ClassifiedSeed::DataRooted { root, expr, .. } => { + let field = extract_data_field_name(root, expr); + if is_le_bytes_expr(expr) { + quote! { &self.seeds.#field } + } else { + quote! { self.seeds.#field.as_ref() } + } + } + ClassifiedSeed::FunctionCall { .. } => { + quote! { + { + panic!("seed_refs_with_bump not supported for function call seeds on packed variant."); + #[allow(unreachable_code)] + { bump_storage as &[u8] } + } + } + } + }) + .collect() + } + + fn generate_pack_seed_fields_pinocchio(&self) -> Vec { + self.seed_fields + .iter() + .map(|sf| { + let field = &sf.field_name; + if sf.is_account_seed { + let idx_field = format_ident!("{}_idx", field); + quote! { #idx_field: accounts.insert_or_get(solana_pubkey::Pubkey::from(self.seeds.#field)) } + } else if sf.has_le_bytes { + quote! { #field: self.seeds.#field.to_le_bytes() } + } else { + quote! { #field: self.seeds.#field } + } + }) + .collect() + } + + // ========================================================================= + // ORIGINAL (ANCHOR) GENERATION METHODS + // ========================================================================= + /// Generate the `{Field}Seeds` struct. fn generate_seeds_struct(&self) -> TokenStream { let struct_name = format_ident!("{}Seeds", self.variant_name); @@ -240,8 +631,8 @@ impl VariantBuilder { // NOTE: pack() is NOT generated here - it's in the Pack trait impl (off-chain only) quote! { - impl light_sdk::interface::LightAccountVariantTrait<#seed_count> for #variant_name { - const PROGRAM_ID: Pubkey = crate::ID; + impl light_account::LightAccountVariantTrait<#seed_count> for #variant_name { + const PROGRAM_ID: [u8; 32] = crate::LIGHT_CPI_SIGNER.program_id; type Seeds = #seeds_struct_name; type Data = #inner_type; @@ -281,24 +672,24 @@ impl VariantBuilder { // Build ProgramPackedAccounts from the accounts slice let unpack_data = quote! { { - let packed_accounts = light_sdk::light_account_checks::packed_accounts::ProgramPackedAccounts { accounts }; - <#inner_type as light_sdk::interface::LightAccount>::unpack(&self.data, &packed_accounts) - .map_err(|_| anchor_lang::error::ErrorCode::InvalidProgramId)? + let packed_accounts = light_account::packed_accounts::ProgramPackedAccounts { accounts }; + <#inner_type as light_account::LightAccount>::unpack(&self.data, &packed_accounts) + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)? } }; quote! { - impl light_sdk::interface::PackedLightAccountVariantTrait<#seed_count> for #packed_variant_name { + impl light_account::PackedLightAccountVariantTrait<#seed_count> for #packed_variant_name { type Unpacked = #variant_name; - const ACCOUNT_TYPE: light_sdk::interface::AccountType = - <#inner_type as light_sdk::interface::LightAccount>::ACCOUNT_TYPE; + const ACCOUNT_TYPE: light_account::AccountType = + <#inner_type as light_account::LightAccount>::ACCOUNT_TYPE; fn bump(&self) -> u8 { self.seeds.bump } - fn unpack(&self, accounts: &[anchor_lang::prelude::AccountInfo]) -> anchor_lang::Result { + fn unpack(&self, accounts: &[AI]) -> std::result::Result { #(#unpack_seed_stmts)* Ok(#variant_name { @@ -309,21 +700,16 @@ impl VariantBuilder { }) } - fn seed_refs_with_bump<'a>( + fn seed_refs_with_bump<'a, AI: light_account::AccountInfoTrait>( &'a self, - accounts: &'a [anchor_lang::prelude::AccountInfo], + accounts: &'a [AI], bump_storage: &'a [u8; 1], - ) -> std::result::Result<[&'a [u8]; #seed_count], solana_program_error::ProgramError> { + ) -> std::result::Result<[&'a [u8]; #seed_count], light_account::LightSdkTypesError> { Ok([#(#packed_seed_refs_items,)* bump_storage]) } - fn into_in_token_data(&self, _tree_info: &light_sdk::instruction::PackedStateTreeInfo, _output_queue_index: u8) -> anchor_lang::Result { - Err(solana_program_error::ProgramError::InvalidAccountData.into()) - } - - fn into_in_tlv(&self) -> anchor_lang::Result>> { - Ok(None) - } + // into_in_token_data and into_in_tlv use default impls from trait + // (return Err/None for PDA variants) } } } @@ -342,22 +728,22 @@ impl VariantBuilder { // Use LightAccount::pack for all accounts (including zero-copy) let pack_data = quote! { - <#inner_type as light_sdk::interface::LightAccount>::pack(&self.data, accounts) - .map_err(|_| solana_program_error::ProgramError::InvalidAccountData)? + <#inner_type as light_account::LightAccount>::pack(&self.data, accounts) + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)? }; quote! { // Pack trait is only available off-chain (client-side packing) #[cfg(not(target_os = "solana"))] - impl light_sdk::Pack for #variant_name { + impl light_account::Pack for #variant_name { type Packed = #packed_variant_name; fn pack( &self, - accounts: &mut light_sdk::instruction::PackedAccounts, - ) -> std::result::Result { - use light_sdk::interface::LightAccountVariantTrait; - let (_, bump) = self.derive_pda(); + accounts: &mut light_account::interface::instruction::PackedAccounts, + ) -> std::result::Result { + use light_account::LightAccountVariantTrait; + let (_, bump) = self.derive_pda::>(); Ok(#packed_variant_name { seeds: #packed_seeds_struct_name { #(#pack_seed_fields,)* @@ -482,7 +868,7 @@ impl VariantBuilder { let field = &sf.field_name; if sf.is_account_seed { let idx_field = format_ident!("{}_idx", field); - quote! { #idx_field: accounts.insert_or_get(self.seeds.#field) } + quote! { #idx_field: accounts.insert_or_get(AM::pubkey_from_bytes(self.seeds.#field.to_bytes())) } } else if sf.has_le_bytes { quote! { #field: self.seeds.#field.to_le_bytes() } } else { @@ -494,7 +880,7 @@ impl VariantBuilder { /// Generate unpack statements to resolve indices to Pubkeys. /// - /// Used in `unpack()` which returns `anchor_lang::Result`. + /// Used in `unpack()` which returns `Result<..., LightSdkTypesError>`. fn generate_unpack_seed_statements(&self, _for_program_error: bool) -> Vec { self.seed_fields .iter() @@ -503,10 +889,12 @@ impl VariantBuilder { let field = &sf.field_name; let idx_field = format_ident!("{}_idx", field); quote! { - let #field = *accounts - .get(self.seeds.#idx_field as usize) - .ok_or(anchor_lang::error::ErrorCode::AccountNotEnoughKeys)? - .key; + let #field = solana_pubkey::Pubkey::new_from_array( + accounts + .get(self.seeds.#idx_field as usize) + .ok_or(light_account::LightSdkTypesError::NotEnoughAccountKeys)? + .key() + ); } }) .collect() @@ -568,9 +956,8 @@ impl VariantBuilder { quote! { accounts .get(self.seeds.#idx_field as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? - .key - .as_ref() + .ok_or(light_account::LightSdkTypesError::InvalidInstructionData)? + .key_ref() } } ClassifiedSeed::DataRooted { root, expr, .. } => { diff --git a/sdk-libs/macros/src/light_pdas/light_account_keywords.rs b/sdk-libs/macros/src/light_pdas/light_account_keywords.rs index bbee9d4853..1550e5a010 100644 --- a/sdk-libs/macros/src/light_pdas/light_account_keywords.rs +++ b/sdk-libs/macros/src/light_pdas/light_account_keywords.rs @@ -22,6 +22,12 @@ //! pub vault: UncheckedAccount<'info>, //! ``` +/// Valid keys for `pda::` namespace in `#[light_account(pda::...)]` attributes. +/// Used by `#[derive(LightProgram)]` enum variants. +/// - `seeds`: PDA seeds for account derivation +/// - `zero_copy`: Flag indicating zero-copy deserialization +pub const PDA_NAMESPACE_KEYS: &[&str] = &["seeds", "zero_copy"]; + /// Valid keys for `token::` namespace in `#[light_account(init, token::...)]` attributes. /// These map to the TokenAccountField struct. /// - `seeds`: Token account PDA seeds (for signing as the token account) - can be dynamic @@ -42,8 +48,6 @@ pub const MINT_NAMESPACE_KEYS: &[&str] = &[ "freeze_authority", "authority_seeds", "authority_bump", - "rent_payment", - "write_top_up", "name", "symbol", "uri", @@ -93,6 +97,7 @@ pub fn is_shorthand_key(namespace: &str, key: &str) -> bool { /// A slice of valid key strings for the namespace. pub fn valid_keys_for_namespace(namespace: &str) -> &'static [&'static str] { match namespace { + "pda" => PDA_NAMESPACE_KEYS, "token" => TOKEN_NAMESPACE_KEYS, "associated_token" => ASSOCIATED_TOKEN_NAMESPACE_KEYS, "mint" => MINT_NAMESPACE_KEYS, @@ -113,7 +118,7 @@ pub fn validate_namespaced_key(namespace: &str, key: &str) -> Result<(), String> if valid_keys.is_empty() { return Err(format!( - "Unknown namespace `{}`. Expected: token, associated_token, or mint", + "Unknown namespace `{}`. Expected: pda, token, associated_token, or mint", namespace )); } @@ -142,7 +147,7 @@ pub fn unknown_key_error(namespace: &str, key: &str) -> String { let valid = valid_keys_for_namespace(namespace); if valid.is_empty() { format!( - "Unknown namespace `{}`. Expected: token, associated_token, or mint", + "Unknown namespace `{}`. Expected: pda, token, associated_token, or mint", namespace ) } else { @@ -175,6 +180,13 @@ pub fn missing_namespace_error(key: &str, account_type: &str) -> String { mod tests { use super::*; + #[test] + fn test_pda_namespace_keys() { + assert!(PDA_NAMESPACE_KEYS.contains(&"seeds")); + assert!(PDA_NAMESPACE_KEYS.contains(&"zero_copy")); + assert!(!PDA_NAMESPACE_KEYS.contains(&"unknown")); + } + #[test] fn test_token_namespace_keys() { assert!(TOKEN_NAMESPACE_KEYS.contains(&"seeds")); @@ -244,6 +256,9 @@ mod tests { #[test] fn test_valid_keys_for_namespace() { + let pda_kw = valid_keys_for_namespace("pda"); + assert_eq!(pda_kw, PDA_NAMESPACE_KEYS); + let token_kw = valid_keys_for_namespace("token"); assert_eq!(token_kw, TOKEN_NAMESPACE_KEYS); diff --git a/sdk-libs/macros/src/light_pdas/parsing/crate_context.rs b/sdk-libs/macros/src/light_pdas/parsing/crate_context.rs index 7ede4a0858..3b34a76287 100644 --- a/sdk-libs/macros/src/light_pdas/parsing/crate_context.rs +++ b/sdk-libs/macros/src/light_pdas/parsing/crate_context.rs @@ -25,6 +25,14 @@ pub struct CrateContext { } impl CrateContext { + /// Create an empty CrateContext (for testing or when no struct discovery is needed). + #[allow(dead_code)] + pub fn empty() -> Self { + CrateContext { + modules: BTreeMap::new(), + } + } + /// Parse all modules starting from the crate root (lib.rs or main.rs). /// /// Uses `CARGO_MANIFEST_DIR` environment variable to locate the crate root. diff --git a/sdk-libs/macros/src/light_pdas/program/compress.rs b/sdk-libs/macros/src/light_pdas/program/compress.rs index 02c14b2933..305c47a8b3 100644 --- a/sdk-libs/macros/src/light_pdas/program/compress.rs +++ b/sdk-libs/macros/src/light_pdas/program/compress.rs @@ -101,7 +101,7 @@ impl CompressBuilder { let pod_bytes = &data[8..8 + core::mem::size_of::<#name>()]; let mut account_data: #name = *bytemuck::from_bytes(pod_bytes); drop(data); - light_sdk::interface::prepare_account_for_compression( + light_account::prepare_account_for_compression( account_info, &mut account_data, meta, index, ctx, ) } @@ -114,9 +114,9 @@ impl CompressBuilder { d if d == #name::LIGHT_DISCRIMINATOR => { let mut reader = &data[8..]; let mut account_data = #name::deserialize(&mut reader) - .map_err(|_| solana_program_error::ProgramError::InvalidAccountData)?; + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)?; drop(data); - light_sdk::interface::prepare_account_for_compression( + light_account::prepare_account_for_compression( account_info, &mut account_data, meta, index, ctx, ) } @@ -127,16 +127,16 @@ impl CompressBuilder { Ok(syn::parse_quote! { fn __compress_dispatch<'info>( account_info: &anchor_lang::prelude::AccountInfo<'info>, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + meta: &light_account::account_meta::CompressedAccountMetaNoLamportsNoAddress, index: usize, - ctx: &mut light_sdk::interface::CompressCtx<'_, 'info>, - ) -> std::result::Result<(), solana_program_error::ProgramError> { - use light_sdk::LightDiscriminator; + ctx: &mut light_account::CompressCtx<'_, 'info>, + ) -> std::result::Result<(), light_account::LightSdkTypesError> { + use light_account::LightDiscriminator; use borsh::BorshDeserialize; let data = account_info.try_borrow_data()?; let discriminator: [u8; 8] = data[..8] .try_into() - .map_err(|_| solana_program_error::ProgramError::InvalidAccountData)?; + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)?; match discriminator { #(#compress_arms)* _ => Ok(()), @@ -151,35 +151,34 @@ impl CompressBuilder { #[inline(never)] pub fn process_compress_accounts_idempotent<'info>( remaining_accounts: &[solana_account_info::AccountInfo<'info>], - instruction_data: &[u8], + params: &light_account::CompressAndCloseParams, ) -> Result<()> { - light_sdk::interface::process_compress_pda_accounts_idempotent( + light_account::process_compress_pda_accounts_idempotent( remaining_accounts, - instruction_data, + params, __compress_dispatch, LIGHT_CPI_SIGNER, - &crate::ID, + &crate::LIGHT_CPI_SIGNER.program_id, ) - .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) + .map_err(|e| anchor_lang::error::Error::from(solana_program_error::ProgramError::from(e))) } }) } /// Generate the compress instruction entrypoint function (v2 interface). /// - /// Accepts `instruction_data: Vec` as a single parameter. - /// The SDK client wraps the serialized data in a Vec (4-byte length prefix), - /// and Anchor deserializes Vec correctly with this format. + /// Accepts typed `CompressAndCloseParams` directly. + /// Anchor deserializes the params from instruction data. pub fn generate_entrypoint(&self) -> Result { Ok(syn::parse_quote! { #[inline(never)] pub fn compress_accounts_idempotent<'info>( ctx: Context<'_, '_, '_, 'info, CompressAccountsIdempotent<'info>>, - instruction_data: Vec, + params: light_account::CompressAndCloseParams, ) -> Result<()> { __processor_functions::process_compress_accounts_idempotent( ctx.remaining_accounts, - &instruction_data, + ¶ms, ) } }) @@ -311,6 +310,182 @@ impl CompressBuilder { }) } + /// Generate compress dispatch as an associated function on the enum. + /// + /// When `#[derive(LightProgram)]` is used, the dispatch function is generated + /// as `impl EnumName { pub fn compress_dispatch(...) }` so it can be referenced + /// as `EnumName::compress_dispatch` and passed to SDK functions. + pub fn generate_enum_dispatch_method(&self, enum_name: &syn::Ident) -> Result { + let compress_arms: Vec<_> = self.accounts.iter().map(|info| { + let name = qualify_type_with_crate(&info.account_type); + + if info.is_zero_copy { + quote! { + d if d == #name::LIGHT_DISCRIMINATOR => { + let pod_bytes = &data[8..8 + core::mem::size_of::<#name>()]; + let mut account_data: #name = *bytemuck::from_bytes(pod_bytes); + drop(data); + light_account::prepare_account_for_compression( + account_info, &mut account_data, meta, index, ctx, + ) + } + } + } else { + quote! { + d if d == #name::LIGHT_DISCRIMINATOR => { + let mut reader = &data[8..]; + let mut account_data = #name::deserialize(&mut reader) + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)?; + drop(data); + light_account::prepare_account_for_compression( + account_info, &mut account_data, meta, index, ctx, + ) + } + } + } + }).collect(); + + Ok(quote! { + impl #enum_name { + pub fn compress_dispatch<'info>( + account_info: &anchor_lang::prelude::AccountInfo<'info>, + meta: &light_account::account_meta::CompressedAccountMetaNoLamportsNoAddress, + index: usize, + ctx: &mut light_account::CompressCtx<'_, 'info>, + ) -> std::result::Result<(), light_account::LightSdkTypesError> { + use light_account::LightDiscriminator; + use borsh::BorshDeserialize; + let data = account_info.try_borrow_data()?; + let discriminator: [u8; 8] = data[..8] + .try_into() + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)?; + match discriminator { + #(#compress_arms)* + _ => Ok(()), + } + } + } + }) + } + + // ------------------------------------------------------------------------- + // Pinocchio Code Generation Methods + // ------------------------------------------------------------------------- + + /// Generate compress dispatch as an associated function on the enum (pinocchio version). + /// + /// Same logic as `generate_enum_dispatch_method()` but with pinocchio types: + /// - `pinocchio::account_info::AccountInfo` instead of `anchor_lang::prelude::AccountInfo` + /// - `light_account_pinocchio::` instead of `light_account::` + pub fn generate_enum_dispatch_method_pinocchio( + &self, + enum_name: &syn::Ident, + ) -> Result { + let compress_arms: Vec<_> = self.accounts.iter().map(|info| { + let name = qualify_type_with_crate(&info.account_type); + + if info.is_zero_copy { + quote! { + d if d == #name::LIGHT_DISCRIMINATOR => { + let pod_bytes = &data[8..8 + core::mem::size_of::<#name>()]; + let mut account_data: #name = *bytemuck::from_bytes(pod_bytes); + drop(data); + light_account_pinocchio::prepare_account_for_compression( + account_info, &mut account_data, meta, index, ctx, + ) + } + } + } else { + quote! { + d if d == #name::LIGHT_DISCRIMINATOR => { + let mut reader = &data[8..]; + let mut account_data = #name::deserialize(&mut reader) + .map_err(|_| light_account_pinocchio::LightSdkTypesError::InvalidInstructionData)?; + drop(data); + light_account_pinocchio::prepare_account_for_compression( + account_info, &mut account_data, meta, index, ctx, + ) + } + } + } + }).collect(); + + Ok(quote! { + impl #enum_name { + pub fn compress_dispatch( + account_info: &pinocchio::account_info::AccountInfo, + meta: &light_account_pinocchio::account_meta::CompressedAccountMetaNoLamportsNoAddress, + index: usize, + ctx: &mut light_account_pinocchio::CompressCtx<'_>, + ) -> std::result::Result<(), light_account_pinocchio::LightSdkTypesError> { + use light_account_pinocchio::LightDiscriminator; + use borsh::BorshDeserialize; + let data = account_info.try_borrow_data() + .map_err(|_| light_account_pinocchio::LightSdkTypesError::Borsh)?; + let discriminator: [u8; 8] = data[..8] + .try_into() + .map_err(|_| light_account_pinocchio::LightSdkTypesError::InvalidInstructionData)?; + match discriminator { + #(#compress_arms)* + _ => Ok(()), + } + } + } + }) + } + + /// Generate `process_compress` as an enum associated function (pinocchio version). + /// + /// The function deserializes params from instruction_data before calling the processor. + pub fn generate_enum_process_compress_pinocchio( + &self, + enum_name: &syn::Ident, + ) -> Result { + Ok(quote! { + impl #enum_name { + pub fn process_compress( + accounts: &[pinocchio::account_info::AccountInfo], + instruction_data: &[u8], + ) -> std::result::Result<(), pinocchio::program_error::ProgramError> { + use borsh::BorshDeserialize; + let params = light_account_pinocchio::CompressAndCloseParams::try_from_slice(instruction_data) + .map_err(|_| pinocchio::program_error::ProgramError::InvalidInstructionData)?; + light_account_pinocchio::process_compress_pda_accounts_idempotent( + accounts, + ¶ms, + Self::compress_dispatch, + crate::LIGHT_CPI_SIGNER, + &crate::LIGHT_CPI_SIGNER.program_id, + ) + .map_err(|e| pinocchio::program_error::ProgramError::Custom(u32::from(e))) + } + } + }) + } + + /// Generate compile-time size validation for compressed accounts (pinocchio version). + /// Uses INIT_SPACE directly instead of CompressedInitSpace trait. + pub fn generate_size_validation_pinocchio(&self) -> Result { + let size_checks: Vec<_> = self.accounts.iter().map(|info| { + let qualified_type = qualify_type_with_crate(&info.account_type); + + // For pinocchio, all types use INIT_SPACE constant (no CompressedInitSpace trait) + quote! { + const _: () = { + const COMPRESSED_SIZE: usize = 8 + #qualified_type::INIT_SPACE; + assert!( + COMPRESSED_SIZE <= 800, + concat!( + "Compressed account '", stringify!(#qualified_type), "' exceeds 800-byte compressible account size limit" + ) + ); + }; + } + }).collect(); + + Ok(quote! { #(#size_checks)* }) + } + /// Generate compile-time size validation for compressed accounts. pub fn generate_size_validation(&self) -> Result { let size_checks: Vec<_> = self.accounts.iter().map(|info| { @@ -332,7 +507,7 @@ impl CompressBuilder { // For Borsh types, use CompressedInitSpace trait quote! { const _: () = { - const COMPRESSED_SIZE: usize = 8 + <#qualified_type as light_sdk::interface::compression_info::CompressedInitSpace>::COMPRESSED_INIT_SPACE; + const COMPRESSED_SIZE: usize = 8 + <#qualified_type as light_account::compression_info::CompressedInitSpace>::COMPRESSED_INIT_SPACE; if COMPRESSED_SIZE > 800 { panic!(concat!( "Compressed account '", stringify!(#qualified_type), "' exceeds 800-byte compressible account size limit. If you need support for larger accounts, send a message to team@lightprotocol.com" diff --git a/sdk-libs/macros/src/light_pdas/program/decompress.rs b/sdk-libs/macros/src/light_pdas/program/decompress.rs index 1cbab4a6b5..dadbbb24f3 100644 --- a/sdk-libs/macros/src/light_pdas/program/decompress.rs +++ b/sdk-libs/macros/src/light_pdas/program/decompress.rs @@ -30,6 +30,10 @@ pub(super) struct DecompressBuilder { pda_ctx_seeds: Vec, /// PDA seed specifications. pda_seeds: Option>, + /// Whether the program has token accounts (tokens/ATAs/mints). + /// When true, the generated processor calls the full decompress function + /// that handles both PDA and token accounts. + has_tokens: bool, } impl DecompressBuilder { @@ -38,10 +42,16 @@ impl DecompressBuilder { /// # Arguments /// * `pda_ctx_seeds` - PDA context seed information for each variant /// * `pda_seeds` - PDA seed specifications - pub fn new(pda_ctx_seeds: Vec, pda_seeds: Option>) -> Self { + /// * `has_tokens` - Whether the program has token accounts + pub fn new( + pda_ctx_seeds: Vec, + pda_seeds: Option>, + has_tokens: bool, + ) -> Self { Self { pda_ctx_seeds, pda_seeds, + has_tokens, } } @@ -50,39 +60,66 @@ impl DecompressBuilder { // ------------------------------------------------------------------------- /// Generate the processor function for decompress accounts (v2 interface). + /// + /// For programs with token accounts, calls the full processor that handles + /// both PDA and token decompression. For PDA-only programs, calls the + /// simpler PDA-only processor. pub fn generate_processor(&self) -> Result { - Ok(syn::parse_quote! { - #[inline(never)] - pub fn process_decompress_accounts_idempotent<'info>( - remaining_accounts: &[solana_account_info::AccountInfo<'info>], - instruction_data: &[u8], - ) -> Result<()> { - light_sdk::interface::process_decompress_pda_accounts_idempotent::( - remaining_accounts, - instruction_data, - LIGHT_CPI_SIGNER, - &crate::ID, - ) - .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) - } - }) + if self.has_tokens { + Ok(syn::parse_quote! { + #[inline(never)] + pub fn process_decompress_accounts_idempotent<'info>( + remaining_accounts: &[solana_account_info::AccountInfo<'info>], + params: &light_account::DecompressIdempotentParams, + ) -> Result<()> { + use solana_program::{clock::Clock, sysvar::Sysvar}; + let current_slot = Clock::get()?.slot; + light_account::process_decompress_accounts_idempotent::<_, PackedLightAccountVariant>( + remaining_accounts, + params, + LIGHT_CPI_SIGNER, + &crate::LIGHT_CPI_SIGNER.program_id, + current_slot, + ) + .map_err(|e| anchor_lang::error::Error::from(solana_program_error::ProgramError::from(e))) + } + }) + } else { + Ok(syn::parse_quote! { + #[inline(never)] + pub fn process_decompress_accounts_idempotent<'info>( + remaining_accounts: &[solana_account_info::AccountInfo<'info>], + params: &light_account::DecompressIdempotentParams, + ) -> Result<()> { + use solana_program::{clock::Clock, sysvar::Sysvar}; + let current_slot = Clock::get()?.slot; + light_account::process_decompress_pda_accounts_idempotent::<_, PackedLightAccountVariant>( + remaining_accounts, + params, + LIGHT_CPI_SIGNER, + &crate::LIGHT_CPI_SIGNER.program_id, + current_slot, + ) + .map_err(|e| anchor_lang::error::Error::from(solana_program_error::ProgramError::from(e))) + } + }) + } } /// Generate the decompress instruction entrypoint function (v2 interface). /// - /// Accepts `instruction_data: Vec` as a single parameter. - /// The SDK client wraps the serialized data in a Vec (4-byte length prefix), - /// and Anchor deserializes Vec correctly with this format. + /// Accepts typed `DecompressIdempotentParams` directly. + /// Anchor deserializes the params from instruction data. pub fn generate_entrypoint(&self) -> Result { Ok(syn::parse_quote! { #[inline(never)] pub fn decompress_accounts_idempotent<'info>( ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, - instruction_data: Vec, + params: light_account::DecompressIdempotentParams, ) -> Result<()> { __processor_functions::process_decompress_accounts_idempotent( ctx.remaining_accounts, - &instruction_data, + ¶ms, ) } }) @@ -216,7 +253,7 @@ impl DecompressBuilder { /// Generate PDA seed provider implementations. /// Returns empty Vec for mint-only or token-only programs that have no PDA seeds. - pub fn generate_seed_provider_impls(&self) -> Result> { + pub fn generate_seed_provider_impls(&self, is_pinocchio: bool) -> Result> { // For mint-only or token-only programs, there are no PDA seeds - return empty Vec let pda_seed_specs = match self.pda_seeds.as_ref() { Some(specs) if !specs.is_empty() => specs, @@ -289,20 +326,26 @@ impl DecompressBuilder { ctx_fields, &ctx_info.state_field_names, params_only_fields, + is_pinocchio, )?; let has_params_only = !params_only_fields.is_empty(); + let account_crate = if is_pinocchio { + quote! { light_account_pinocchio } + } else { + quote! { light_account } + }; let seed_params_impl = if has_params_only { quote! { #ctx_seeds_struct - impl light_sdk::interface::PdaSeedDerivation<#ctx_seeds_struct_name, SeedParams> for #inner_type { + impl #account_crate::PdaSeedDerivation<#ctx_seeds_struct_name, SeedParams> for #inner_type { fn derive_pda_seeds_with_accounts( &self, - program_id: &solana_pubkey::Pubkey, + program_id: &[u8; 32], ctx_seeds: &#ctx_seeds_struct_name, seed_params: &SeedParams, - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { + ) -> std::result::Result<(Vec>, [u8; 32]), #account_crate::LightSdkTypesError> { #seed_derivation } } @@ -311,13 +354,13 @@ impl DecompressBuilder { quote! { #ctx_seeds_struct - impl light_sdk::interface::PdaSeedDerivation<#ctx_seeds_struct_name, SeedParams> for #inner_type { + impl #account_crate::PdaSeedDerivation<#ctx_seeds_struct_name, SeedParams> for #inner_type { fn derive_pda_seeds_with_accounts( &self, - program_id: &solana_pubkey::Pubkey, + program_id: &[u8; 32], ctx_seeds: &#ctx_seeds_struct_name, _seed_params: &SeedParams, - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { + ) -> std::result::Result<(Vec>, [u8; 32]), #account_crate::LightSdkTypesError> { #seed_derivation } } @@ -328,6 +371,105 @@ impl DecompressBuilder { Ok(results) } + + // ------------------------------------------------------------------------- + // Pinocchio Code Generation Methods + // ------------------------------------------------------------------------- + + /// Generate `process_decompress` as an enum associated function (pinocchio version). + /// + /// The function deserializes params from instruction_data before calling the processor. + pub fn generate_enum_process_decompress_pinocchio( + &self, + enum_name: &syn::Ident, + ) -> Result { + let processor_call = if self.has_tokens { + quote! { + light_account_pinocchio::process_decompress_accounts_idempotent::<_, PackedLightAccountVariant>( + accounts, + ¶ms, + crate::LIGHT_CPI_SIGNER, + &crate::LIGHT_CPI_SIGNER.program_id, + current_slot, + ) + } + } else { + quote! { + light_account_pinocchio::process_decompress_pda_accounts_idempotent::<_, PackedLightAccountVariant>( + accounts, + ¶ms, + crate::LIGHT_CPI_SIGNER, + &crate::LIGHT_CPI_SIGNER.program_id, + current_slot, + ) + } + }; + + Ok(quote! { + impl #enum_name { + pub fn process_decompress( + accounts: &[pinocchio::account_info::AccountInfo], + instruction_data: &[u8], + ) -> std::result::Result<(), pinocchio::program_error::ProgramError> { + use borsh::BorshDeserialize; + use pinocchio::sysvars::Sysvar; + let params = light_account_pinocchio::DecompressIdempotentParams::::try_from_slice(instruction_data) + .map_err(|_| pinocchio::program_error::ProgramError::InvalidInstructionData)?; + let current_slot = pinocchio::sysvars::clock::Clock::get() + .map_err(|_| pinocchio::program_error::ProgramError::UnsupportedSysvar)? + .slot; + #processor_call + .map_err(|e| pinocchio::program_error::ProgramError::Custom(u32::from(e))) + } + } + }) + } + + /// Generate decompress dispatch as an associated function on the enum. + /// + /// When `#[derive(LightProgram)]` is used, the dispatch function is generated + /// as `impl EnumName { pub fn decompress_dispatch(...) }` so it can be referenced + /// as `EnumName::decompress_dispatch`. + /// + /// This wraps the type-parameter-based SDK call, binding `PackedLightAccountVariant` + /// as the concrete type. + pub fn generate_enum_decompress_dispatch(&self, enum_name: &syn::Ident) -> Result { + let processor_call = if self.has_tokens { + quote! { + light_account::process_decompress_accounts_idempotent::<_, PackedLightAccountVariant>( + remaining_accounts, + params, + cpi_signer, + program_id, + current_slot, + ) + } + } else { + quote! { + light_account::process_decompress_pda_accounts_idempotent::<_, PackedLightAccountVariant>( + remaining_accounts, + params, + cpi_signer, + program_id, + current_slot, + ) + } + }; + + Ok(quote! { + impl #enum_name { + pub fn decompress_dispatch<'info>( + remaining_accounts: &[solana_account_info::AccountInfo<'info>], + params: &light_account::DecompressIdempotentParams, + cpi_signer: light_account::CpiSigner, + program_id: &[u8; 32], + current_slot: u64, + ) -> std::result::Result<(), light_account::LightSdkTypesError> { + #processor_call + } + } + }) + } } // ============================================================================= @@ -344,7 +486,13 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( ctx_seed_fields: &[syn::Ident], state_field_names: &std::collections::HashSet, params_only_fields: &[(syn::Ident, syn::Type, bool)], + is_pinocchio: bool, ) -> Result { + let account_crate = if is_pinocchio { + quote! { light_account_pinocchio } + } else { + quote! { light_account } + }; // Build a lookup for params-only field names let params_only_names: std::collections::HashSet = params_only_fields .iter() @@ -425,7 +573,7 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( ); bindings.push(quote! { let #binding_name = seed_params.#field_ident - .ok_or(solana_program_error::ProgramError::InvalidAccountData)?; + .ok_or(#account_crate::LightSdkTypesError::InvalidInstructionData)?; let #bytes_binding_name = #binding_name.to_le_bytes(); }); seed_refs.push(quote! { #bytes_binding_name.as_ref() }); @@ -433,7 +581,7 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( // Pubkey field bindings.push(quote! { let #binding_name = seed_params.#field_ident - .ok_or(solana_program_error::ProgramError::InvalidAccountData)?; + .ok_or(#account_crate::LightSdkTypesError::InvalidInstructionData)?; }); seed_refs.push(quote! { #binding_name.as_ref() }); } @@ -468,7 +616,8 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( Ok(quote! { #(#bindings)* let seeds: &[&[u8]] = &[#(#seed_refs,)*]; - let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id); + let program_id_pubkey = solana_pubkey::Pubkey::from(*program_id); + let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &program_id_pubkey); let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); #( seeds_vec.push(seeds[#indices].to_vec()); @@ -479,7 +628,7 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( bump_vec.push(bump); seeds_vec.push(bump_vec); } - Ok((seeds_vec, pda)) + Ok((seeds_vec, pda.to_bytes())) }) } diff --git a/sdk-libs/macros/src/light_pdas/program/derive_light_program.rs b/sdk-libs/macros/src/light_pdas/program/derive_light_program.rs new file mode 100644 index 0000000000..3d4f3fe89c --- /dev/null +++ b/sdk-libs/macros/src/light_pdas/program/derive_light_program.rs @@ -0,0 +1,1770 @@ +//! Manual `#[derive(LightProgram)]` macro implementation. +//! +//! Allows specifying compressed account variants on an enum, generating equivalent +//! code to `#[light_program]` auto-discovery. Useful for external programs where +//! you can't add `#[light_program]` to the module. +//! +//! ## Syntax +//! +//! ```ignore +//! #[derive(LightProgram)] +//! pub enum ProgramAccounts { +//! #[light_account(pda::seeds = [b"record", ctx.owner])] +//! Record(MinimalRecord), +//! +//! #[light_account(pda::seeds = [RECORD_SEED, ctx.owner], pda::zero_copy)] +//! ZeroCopyRecord(ZeroCopyRecord), +//! +//! #[light_account(token::seeds = [VAULT_SEED, ctx.mint], token::owner_seeds = [VAULT_AUTH_SEED])] +//! Vault, +//! +//! #[light_account(associated_token)] +//! Ata, +//! } +//! ``` + +use std::collections::HashSet; + +use proc_macro2::{Span, TokenStream}; +use quote::quote; +use syn::{parse::ParseStream, DeriveInput, Ident, Result, Token, Type}; + +use super::instructions::{ + generate_light_program_items, CompressibleAccountInfo, InstructionDataSpec, SeedElement, + TokenSeedSpec, +}; +use crate::light_pdas::{ + accounts::variant::VariantBuilder, + light_account_keywords::validate_namespaced_key, + parsing::CrateContext, + seeds::{ClassifiedSeed, ExtractedSeedSpec}, + shared_utils::is_constant_identifier, +}; + +// ============================================================================= +// PARSING TYPES +// ============================================================================= + +/// Kind of a manual variant in the enum. +#[derive(Clone, Debug)] +enum ManualVariantKind { + Pda, + Token, + Ata, +} + +/// A single seed element parsed from the attribute. +#[derive(Clone, Debug)] +enum ManualSeed { + /// b"literal" - byte string literal + ByteLiteral(syn::LitByteStr), + /// "literal" - string literal (converted to bytes) + StrLiteral(syn::LitStr), + /// CONSTANT or path::CONSTANT + Constant(syn::Path), + /// ctx.field - context account reference + CtxField(Ident), + /// data.field - instruction data reference + DataField(Ident), +} + +/// Parsed variant from the manual enum. +#[derive(Clone, Debug)] +struct ParsedManualVariant { + ident: Ident, + kind: ManualVariantKind, + inner_type: Option, + is_zero_copy: bool, + seeds: Vec, + owner_seeds: Option>, +} + +// ============================================================================= +// PARSING +// ============================================================================= + +/// Parse all variants from the derive input enum. +fn parse_enum_variants(input: &DeriveInput) -> Result> { + let data = match &input.data { + syn::Data::Enum(data) => data, + _ => { + return Err(syn::Error::new_spanned( + input, + "#[derive(LightProgram)] can only be applied to enums", + )) + } + }; + + let mut variants = Vec::new(); + for variant in &data.variants { + // Find the #[light_account(...)] attribute + let attr = variant + .attrs + .iter() + .find(|a| a.path().is_ident("light_account")) + .ok_or_else(|| { + syn::Error::new_spanned( + &variant.ident, + format!( + "Variant '{}' is missing #[light_account(...)] attribute", + variant.ident + ), + ) + })?; + + let parsed = parse_variant_attr(attr, &variant.ident, &variant.fields)?; + variants.push(parsed); + } + + Ok(variants) +} + +/// Parse a single variant's `#[light_account(...)]` attribute. +fn parse_variant_attr( + attr: &syn::Attribute, + variant_ident: &Ident, + fields: &syn::Fields, +) -> Result { + let tokens: TokenStream = attr.parse_args()?; + let parsed: VariantAttrContent = syn::parse2(tokens)?; + + // Extract inner type from tuple field for PDA variants + let inner_type = match &parsed.kind { + ManualVariantKind::Pda => { + let ty = extract_inner_type(variant_ident, fields)?; + Some(ty) + } + ManualVariantKind::Token | ManualVariantKind::Ata => { + // Ensure unit variant + if !matches!(fields, syn::Fields::Unit) { + return Err(syn::Error::new_spanned( + variant_ident, + format!( + "Token/ATA variant '{}' must be a unit variant (no fields)", + variant_ident + ), + )); + } + None + } + }; + + Ok(ParsedManualVariant { + ident: variant_ident.clone(), + kind: parsed.kind, + inner_type, + is_zero_copy: parsed.is_zero_copy, + seeds: parsed.seeds, + owner_seeds: parsed.owner_seeds, + }) +} + +/// Extract inner type from a tuple variant's first field. +fn extract_inner_type(variant_ident: &Ident, fields: &syn::Fields) -> Result { + match fields { + syn::Fields::Unnamed(unnamed) => { + if unnamed.unnamed.len() != 1 { + return Err(syn::Error::new_spanned( + variant_ident, + format!( + "PDA variant '{}' must have exactly one field (the data type)", + variant_ident + ), + )); + } + Ok(unnamed.unnamed[0].ty.clone()) + } + _ => Err(syn::Error::new_spanned( + variant_ident, + format!( + "PDA variant '{}' must be a tuple variant with the data type, e.g., {}(MyRecord)", + variant_ident, variant_ident + ), + )), + } +} + +// ============================================================================= +// ATTRIBUTE CONTENT PARSING +// ============================================================================= + +/// Parsed content of `#[light_account(...)]`. +/// +/// Kind is inferred from namespace prefix or standalone keyword: +/// - Any `pda::*` present -> PDA +/// - Any `token::*` present -> Token +/// - `associated_token` -> ATA +struct VariantAttrContent { + kind: ManualVariantKind, + is_zero_copy: bool, + seeds: Vec, + owner_seeds: Option>, +} + +/// Tracks seen keywords/namespaces to detect duplicates and conflicts. +#[derive(Default)] +struct SeenDeriveKeywords { + namespace: Option, + seen_keys: HashSet, +} + +impl SeenDeriveKeywords { + /// Record a namespaced key. Returns error on mixed namespaces or duplicate keys. + fn add_namespaced_key(&mut self, ns: &Ident, key: &Ident) -> Result<()> { + let ns_str = ns.to_string(); + let key_str = key.to_string(); + + if let Err(err_msg) = validate_namespaced_key(&ns_str, &key_str) { + return Err(syn::Error::new_spanned(key, err_msg)); + } + + if let Some(ref prev_ns) = self.namespace { + if prev_ns != &ns_str { + return Err(syn::Error::new_spanned( + ns, + format!( + "Mixed namespaces: `{}::` conflicts with previous `{}::`. \ + Each variant must use a single namespace.", + ns_str, prev_ns + ), + )); + } + } else { + self.namespace = Some(ns_str.clone()); + } + + if !self.seen_keys.insert(key_str.clone()) { + return Err(syn::Error::new_spanned( + key, + format!( + "Duplicate key `{}::{}`. Each key can only appear once.", + ns_str, key_str + ), + )); + } + + Ok(()) + } +} + +/// Map namespace ident to ManualVariantKind. +fn infer_kind_from_namespace(ns: &Ident) -> Result { + match ns.to_string().as_str() { + "pda" => Ok(ManualVariantKind::Pda), + "token" => Ok(ManualVariantKind::Token), + _ => Err(syn::Error::new_spanned( + ns, + format!( + "Unknown namespace `{}` for #[derive(LightProgram)]. \ + Expected: `pda` or `token`. For ATA use `associated_token`. \ + Mints are decompressed directly with the Light Token Program \ + and don't need to be declared here.", + ns + ), + )), + } +} + +/// Parse the value part of a namespaced key. +fn parse_namespaced_value( + ns: &Ident, + key: &Ident, + input: ParseStream, + seeds: &mut Vec, + owner_seeds: &mut Option>, + is_zero_copy: &mut bool, +) -> Result<()> { + let ns_str = ns.to_string(); + let key_str = key.to_string(); + + match (ns_str.as_str(), key_str.as_str()) { + ("pda", "seeds") => { + input.parse::()?; + *seeds = parse_seed_array(input)?; + } + ("pda", "zero_copy") => { + *is_zero_copy = true; + } + ("token", "seeds") => { + input.parse::()?; + *seeds = parse_seed_array(input)?; + } + ("token", "owner_seeds") => { + input.parse::()?; + *owner_seeds = Some(parse_seed_array(input)?); + } + _ => { + return Err(syn::Error::new_spanned( + key, + format!( + "Unsupported key `{}::{}` in #[derive(LightProgram)]", + ns_str, key_str + ), + )); + } + } + Ok(()) +} + +impl syn::parse::Parse for VariantAttrContent { + fn parse(input: ParseStream) -> Result { + let mut seen = SeenDeriveKeywords::default(); + let mut is_zero_copy = false; + let mut seeds = Vec::new(); + let mut owner_seeds = None; + + // Parse first token to determine kind + let first: Ident = input.parse()?; + + let kind = if first == "associated_token" { + ManualVariantKind::Ata + } else if input.peek(Token![::]) { + // Namespaced key: pda::seeds, token::seeds, etc. + input.parse::()?; + let key: Ident = input.parse()?; + + seen.add_namespaced_key(&first, &key)?; + let k = infer_kind_from_namespace(&first)?; + + parse_namespaced_value( + &first, + &key, + input, + &mut seeds, + &mut owner_seeds, + &mut is_zero_copy, + )?; + k + } else { + return Err(syn::Error::new_spanned( + &first, + format!( + "Unknown keyword `{}`. Expected: `associated_token` \ + or namespaced key like `pda::seeds`, `token::seeds`. \ + Mints are decompressed directly with the Light Token Program \ + and don't need to be declared here.", + first + ), + )); + }; + + // Parse remaining comma-separated items + while input.peek(Token![,]) { + input.parse::()?; + if input.is_empty() { + break; + } + + let ident: Ident = input.parse()?; + + if !input.peek(Token![::]) { + return Err(syn::Error::new_spanned( + &ident, + format!( + "Unexpected keyword `{}`. Use namespaced syntax: `pda::{}` or `token::{}`", + ident, ident, ident + ), + )); + } + + input.parse::()?; + let key: Ident = input.parse()?; + + seen.add_namespaced_key(&ident, &key)?; + + parse_namespaced_value( + &ident, + &key, + input, + &mut seeds, + &mut owner_seeds, + &mut is_zero_copy, + )?; + } + + // Post-parse validation + + match kind { + ManualVariantKind::Pda => { + if seeds.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + "PDA variant requires `pda::seeds = [...]`", + )); + } + } + ManualVariantKind::Token => { + if seeds.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + "Token variant requires `token::seeds = [...]`", + )); + } + if owner_seeds.is_none() { + return Err(syn::Error::new( + Span::call_site(), + "Token variant requires `token::owner_seeds = [...]`", + )); + } + } + ManualVariantKind::Ata => {} + } + + Ok(VariantAttrContent { + kind, + is_zero_copy, + seeds, + owner_seeds, + }) + } +} + +/// Parse a seed array `[seed1, seed2, ...]`. +fn parse_seed_array(input: syn::parse::ParseStream) -> Result> { + let content; + syn::bracketed!(content in input); + + let mut seeds = Vec::new(); + while !content.is_empty() { + seeds.push(parse_single_seed(&content)?); + if content.peek(syn::Token![,]) { + let _: syn::Token![,] = content.parse()?; + } else { + break; + } + } + Ok(seeds) +} + +/// Parse a single seed expression with explicit prefix disambiguation. +fn parse_single_seed(input: syn::parse::ParseStream) -> Result { + // Check for byte string literal: b"..." + if input.peek(syn::LitByteStr) { + let lit: syn::LitByteStr = input.parse()?; + return Ok(ManualSeed::ByteLiteral(lit)); + } + + // Check for string literal: "..." + if input.peek(syn::LitStr) { + let lit: syn::LitStr = input.parse()?; + return Ok(ManualSeed::StrLiteral(lit)); + } + + // Parse as path/expression + // Could be: ctx.field, data.field, CONSTANT, path::CONSTANT + let expr: syn::Expr = input.parse()?; + classify_seed_expr(&expr) +} + +/// Classify a parsed expression into a ManualSeed. +fn classify_seed_expr(expr: &syn::Expr) -> Result { + match expr { + // ctx.field or data.field + syn::Expr::Field(field_expr) => { + if let syn::Expr::Path(base_path) = field_expr.base.as_ref() { + if let Some(base_ident) = base_path.path.get_ident() { + let base_str = base_ident.to_string(); + if let syn::Member::Named(field_name) = &field_expr.member { + if base_str == "ctx" { + return Ok(ManualSeed::CtxField(field_name.clone())); + } else if base_str == "data" { + return Ok(ManualSeed::DataField(field_name.clone())); + } + } + } + } + Err(syn::Error::new_spanned( + expr, + "Field access seeds must use ctx.field or data.field prefix", + )) + } + // CONSTANT or path::CONSTANT + syn::Expr::Path(path_expr) => { + let path = &path_expr.path; + // Check if last segment is a constant (SCREAMING_SNAKE_CASE) + if let Some(last_seg) = path.segments.last() { + if is_constant_identifier(&last_seg.ident.to_string()) { + return Ok(ManualSeed::Constant(path.clone())); + } + } + // Could be a single lowercase ident like `ctx` or `data` without field access + Err(syn::Error::new_spanned( + expr, + "Seed path must be a SCREAMING_SNAKE_CASE constant, or use ctx.field / data.field prefix", + )) + } + _ => Err(syn::Error::new_spanned( + expr, + "Unsupported seed expression. Use: b\"literal\", \"literal\", ctx.field, data.field, or CONSTANT", + )), + } +} + +// ============================================================================= +// CONVERSION: ManualSeed -> ClassifiedSeed +// ============================================================================= + +fn manual_seed_to_classified(seed: &ManualSeed) -> ClassifiedSeed { + match seed { + ManualSeed::ByteLiteral(lit) => ClassifiedSeed::Literal(lit.value()), + ManualSeed::StrLiteral(lit) => ClassifiedSeed::Literal(lit.value().into_bytes()), + ManualSeed::Constant(path) => { + let expr: syn::Expr = syn::parse_quote!(#path); + ClassifiedSeed::Constant { + path: path.clone(), + expr: Box::new(expr), + } + } + ManualSeed::CtxField(ident) => ClassifiedSeed::CtxRooted { + account: ident.clone(), + }, + ManualSeed::DataField(ident) => { + let expr: syn::Expr = syn::parse_quote!(data.#ident); + ClassifiedSeed::DataRooted { + root: ident.clone(), + expr: Box::new(expr), + } + } + } +} + +// ============================================================================= +// CONVERSION: ManualSeed -> SeedElement +// ============================================================================= + +fn manual_seed_to_seed_element(seed: &ManualSeed) -> SeedElement { + match seed { + ManualSeed::ByteLiteral(lit) => { + let expr: syn::Expr = syn::parse_quote!(#lit); + SeedElement::Expression(Box::new(expr)) + } + ManualSeed::StrLiteral(lit) => SeedElement::Literal(lit.clone()), + ManualSeed::Constant(path) => { + let expr: syn::Expr = syn::parse_quote!(#path); + SeedElement::Expression(Box::new(expr)) + } + ManualSeed::CtxField(ident) => { + let expr: syn::Expr = syn::parse_quote!(ctx.#ident); + SeedElement::Expression(Box::new(expr)) + } + ManualSeed::DataField(ident) => { + let expr: syn::Expr = syn::parse_quote!(data.#ident); + SeedElement::Expression(Box::new(expr)) + } + } +} + +fn manual_seeds_to_punctuated( + seeds: &[ManualSeed], +) -> syn::punctuated::Punctuated { + let mut result = syn::punctuated::Punctuated::new(); + for seed in seeds { + result.push(manual_seed_to_seed_element(seed)); + } + result +} + +fn manual_seeds_to_seed_elements_vec(seeds: &[ManualSeed]) -> Vec { + seeds.iter().map(manual_seed_to_seed_element).collect() +} + +// ============================================================================= +// BUILDER: Convert parsed variants to intermediate types +// ============================================================================= + +#[allow(clippy::type_complexity)] +fn build_intermediate_types( + variants: &[ParsedManualVariant], + _crate_ctx: &CrateContext, +) -> Result<( + Vec, + Option>, + Option>, + Vec, + bool, + bool, + TokenStream, +)> { + let mut compressible_accounts = Vec::new(); + let mut pda_seed_specs = Vec::new(); + let mut token_seed_specs = Vec::new(); + let mut instruction_data_specs = Vec::new(); + let has_mint_fields = false; + let mut has_ata_fields = false; + let mut pda_variant_code = TokenStream::new(); + + // Track data field names we've already added to instruction_data + let mut seen_data_fields = std::collections::HashSet::new(); + + for variant in variants { + match &variant.kind { + ManualVariantKind::Pda => { + let inner_type = variant.inner_type.as_ref().unwrap(); + + // Build CompressibleAccountInfo + compressible_accounts.push(CompressibleAccountInfo { + account_type: inner_type.clone(), + is_zero_copy: variant.is_zero_copy, + }); + + // Build ClassifiedSeeds for VariantBuilder + let classified_seeds: Vec = variant + .seeds + .iter() + .map(manual_seed_to_classified) + .collect(); + + // Build ExtractedSeedSpec for VariantBuilder + let extracted_spec = ExtractedSeedSpec { + variant_name: variant.ident.clone(), + inner_type: inner_type.clone(), + seeds: classified_seeds, + is_zero_copy: variant.is_zero_copy, + struct_name: variant.ident.to_string(), + module_path: "crate".to_string(), + }; + + // Generate variant code + let builder = VariantBuilder::from_extracted_spec(&extracted_spec); + pda_variant_code.extend(builder.build()); + + // Build TokenSeedSpec for PDA seeds + let seed_elements = manual_seeds_to_punctuated(&variant.seeds); + let dummy_eq: syn::Token![=] = syn::parse_quote!(=); + pda_seed_specs.push(TokenSeedSpec { + variant: variant.ident.clone(), + _eq: dummy_eq, + is_token: Some(false), + seeds: seed_elements, + owner_seeds: None, + inner_type: Some(inner_type.clone()), + is_zero_copy: variant.is_zero_copy, + }); + + // Extract data fields for InstructionDataSpec + for seed in &variant.seeds { + if let ManualSeed::DataField(ident) = seed { + let name = ident.to_string(); + if seen_data_fields.insert(name) { + // Default to Pubkey type for data seeds without conversion + instruction_data_specs.push(InstructionDataSpec { + field_name: ident.clone(), + field_type: syn::parse_quote!(Pubkey), + }); + } + } + } + } + + ManualVariantKind::Token => { + let seed_elements = manual_seeds_to_punctuated(&variant.seeds); + let owner_seeds_elements = variant + .owner_seeds + .as_ref() + .map(|os| manual_seeds_to_seed_elements_vec(os)); + + let dummy_eq: syn::Token![=] = syn::parse_quote!(=); + token_seed_specs.push(TokenSeedSpec { + variant: variant.ident.clone(), + _eq: dummy_eq, + is_token: Some(true), + seeds: seed_elements, + owner_seeds: owner_seeds_elements, + inner_type: None, + is_zero_copy: false, + }); + } + + ManualVariantKind::Ata => { + has_ata_fields = true; + } + } + } + + let pda_seeds = if pda_seed_specs.is_empty() { + None + } else { + Some(pda_seed_specs) + }; + + let token_seeds = if token_seed_specs.is_empty() { + None + } else { + Some(token_seed_specs) + }; + + Ok(( + compressible_accounts, + pda_seeds, + token_seeds, + instruction_data_specs, + has_mint_fields, + has_ata_fields, + pda_variant_code, + )) +} + +// ============================================================================= +// ENTRY POINT +// ============================================================================= + +/// Main entry point for `#[derive(LightProgram)]`. +pub fn derive_light_program_impl(input: DeriveInput) -> Result { + // 1. Parse the enum variants + let variants = parse_enum_variants(&input)?; + + if variants.is_empty() { + return Err(syn::Error::new_spanned( + &input, + "#[derive(LightProgram)] enum must have at least one variant", + )); + } + + // 2. Parse crate context for struct field lookup + let crate_ctx = CrateContext::parse_from_manifest()?; + + // 3. Build intermediate types + let ( + compressible_accounts, + pda_seeds, + token_seeds, + instruction_data, + has_mint_fields, + has_ata_fields, + pda_variant_code, + ) = build_intermediate_types(&variants, &crate_ctx)?; + + // 4. Generate all items using the shared function + let enum_name = &input.ident; + let items = generate_light_program_items( + compressible_accounts, + pda_seeds, + token_seeds, + instruction_data, + &crate_ctx, + has_mint_fields, + has_ata_fields, + pda_variant_code, + Some(enum_name), + )?; + + // 5. Combine into single TokenStream + // The derive output appears at the call site, so add the anchor import + let anchor_import = quote! { + use anchor_lang::prelude::*; + }; + + let mut output = TokenStream::new(); + output.extend(anchor_import); + for item in items { + output.extend(item); + } + + Ok(output) +} + +/// Main entry point for `#[derive(LightProgramPinocchio)]`. +/// +/// Same logic as `derive_light_program_impl()` but generates pinocchio-compatible code: +/// - `BorshSerialize/BorshDeserialize` instead of `AnchorSerialize/AnchorDeserialize` +/// - `light_account_pinocchio::` instead of `light_account::` +/// - No `use anchor_lang::prelude::*;` import +/// - Config/compress/decompress as enum associated functions +pub fn derive_light_program_pinocchio_impl(input: DeriveInput) -> Result { + // 1. Parse the enum variants (reused) + let variants = parse_enum_variants(&input)?; + + if variants.is_empty() { + return Err(syn::Error::new_spanned( + &input, + "#[derive(LightProgramPinocchio)] enum must have at least one variant", + )); + } + + // 2. Parse crate context for struct field lookup + let crate_ctx = CrateContext::parse_from_manifest()?; + + // 3. Build intermediate types (reused) + let ( + compressible_accounts, + pda_seeds, + token_seeds, + instruction_data, + has_mint_fields, + has_ata_fields, + _pda_variant_code, // We'll regenerate with pinocchio derives + ) = build_intermediate_types(&variants, &crate_ctx)?; + + // 3b. Re-generate PDA variant code with pinocchio derives + let pda_variant_code_pinocchio: TokenStream = variants + .iter() + .filter(|v| matches!(v.kind, ManualVariantKind::Pda)) + .map(|variant| { + let spec = manual_variant_to_extracted_spec(variant, &crate_ctx); + VariantBuilder::from_extracted_spec(&spec).build_for_pinocchio() + }) + .collect(); + + // 4. Generate all items using the pinocchio orchestration function + let enum_name = &input.ident; + let items = super::instructions::generate_light_program_pinocchio_items( + compressible_accounts, + pda_seeds, + token_seeds, + instruction_data, + &crate_ctx, + has_mint_fields, + has_ata_fields, + pda_variant_code_pinocchio, + Some(enum_name), + )?; + + // 5. Combine into single TokenStream (NO anchor import) + let mut output = TokenStream::new(); + for item in items { + output.extend(item); + } + + Ok(output) +} + +/// Convert a ParsedManualVariant to ExtractedSeedSpec for VariantBuilder. +fn manual_variant_to_extracted_spec( + variant: &ParsedManualVariant, + _crate_ctx: &CrateContext, +) -> ExtractedSeedSpec { + let seeds: Vec = variant + .seeds + .iter() + .map(manual_seed_to_classified) + .collect(); + + ExtractedSeedSpec { + struct_name: variant.ident.to_string(), + variant_name: variant.ident.clone(), + inner_type: variant + .inner_type + .clone() + .unwrap_or_else(|| syn::parse_quote!(())), + seeds, + is_zero_copy: variant.is_zero_copy, + module_path: String::new(), + } +} + +// ============================================================================= +// TESTS +// ============================================================================= + +#[cfg(test)] +mod tests { + use quote::format_ident; + + use super::*; + + fn parse_derive_input(input: &str) -> DeriveInput { + syn::parse_str(input).expect("Failed to parse derive input") + } + + // ========================================================================= + // PARSING TESTS: new #[light_account(...)] namespace syntax + // ========================================================================= + + #[test] + fn test_parse_pda_variant() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"record", ctx.owner])] + Record(MinimalRecord), + } + "#, + ); + + let variants = parse_enum_variants(&input).expect("should parse"); + assert_eq!(variants.len(), 1); + assert_eq!(variants[0].ident.to_string(), "Record"); + assert!(matches!(variants[0].kind, ManualVariantKind::Pda)); + assert!(!variants[0].is_zero_copy); + assert_eq!(variants[0].seeds.len(), 2); + assert!(variants[0].inner_type.is_some()); + } + + #[test] + fn test_parse_zero_copy_variant() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"zc_record", ctx.owner], pda::zero_copy)] + ZcRecord(ZeroCopyRecord), + } + "#, + ); + + let variants = parse_enum_variants(&input).expect("should parse"); + assert_eq!(variants.len(), 1); + assert!(variants[0].is_zero_copy); + } + + #[test] + fn test_parse_token_variant() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(token::seeds = [VAULT_SEED, ctx.mint], token::owner_seeds = [AUTH_SEED])] + Vault, + } + "#, + ); + + let variants = parse_enum_variants(&input).expect("should parse"); + assert_eq!(variants.len(), 1); + assert!(matches!(variants[0].kind, ManualVariantKind::Token)); + assert!(variants[0].inner_type.is_none()); + assert_eq!(variants[0].seeds.len(), 2); + assert!(variants[0].owner_seeds.is_some()); + } + + #[test] + fn test_parse_ata_variant() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(associated_token)] + Ata, + } + "#, + ); + + let variants = parse_enum_variants(&input).expect("should parse"); + assert_eq!(variants.len(), 1); + assert!(matches!(variants[0].kind, ManualVariantKind::Ata)); + } + + #[test] + fn test_parse_mixed_enum() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"record", ctx.owner])] + Record(MinimalRecord), + + #[light_account(token::seeds = [VAULT_SEED, ctx.mint], token::owner_seeds = [AUTH_SEED])] + Vault, + + #[light_account(associated_token)] + Ata, + } + "#, + ); + + let variants = parse_enum_variants(&input).expect("should parse"); + assert_eq!(variants.len(), 3); + } + + #[test] + fn test_error_missing_inner_type_for_pda() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"record", ctx.owner])] + Record, + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("tuple variant"), "Error: {}", err_msg); + } + + #[test] + fn test_error_missing_seeds_for_pda() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::zero_copy)] + Record(MinimalRecord), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("pda::seeds"), "Error: {}", err_msg); + } + + #[test] + fn test_error_missing_attribute() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + Record(MinimalRecord), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + } + + #[test] + fn test_seed_classification() { + // Test byte literal + let byte_lit: syn::LitByteStr = syn::parse_quote!(b"seed"); + let seed = ManualSeed::ByteLiteral(byte_lit); + let classified = manual_seed_to_classified(&seed); + assert!(matches!(classified, ClassifiedSeed::Literal(_))); + + // Test string literal + let str_lit: syn::LitStr = syn::parse_quote!("seed"); + let seed = ManualSeed::StrLiteral(str_lit); + let classified = manual_seed_to_classified(&seed); + assert!(matches!(classified, ClassifiedSeed::Literal(_))); + + // Test constant + let path: syn::Path = syn::parse_quote!(MY_SEED); + let seed = ManualSeed::Constant(path); + let classified = manual_seed_to_classified(&seed); + assert!(matches!(classified, ClassifiedSeed::Constant { .. })); + + // Test ctx field + let ident = format_ident!("owner"); + let seed = ManualSeed::CtxField(ident); + let classified = manual_seed_to_classified(&seed); + assert!(matches!(classified, ClassifiedSeed::CtxRooted { .. })); + + // Test data field + let ident = format_ident!("owner"); + let seed = ManualSeed::DataField(ident); + let classified = manual_seed_to_classified(&seed); + assert!(matches!(classified, ClassifiedSeed::DataRooted { .. })); + } + + #[test] + fn test_string_literal_seeds() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = ["record_seed", ctx.owner])] + Record(MinimalRecord), + } + "#, + ); + + let variants = parse_enum_variants(&input).expect("should parse"); + assert_eq!(variants[0].seeds.len(), 2); + assert!(matches!(variants[0].seeds[0], ManualSeed::StrLiteral(_))); + } + + #[test] + fn test_data_field_seeds() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"record", data.some_key])] + Record(MinimalRecord), + } + "#, + ); + + let variants = parse_enum_variants(&input).expect("should parse"); + assert_eq!(variants[0].seeds.len(), 2); + assert!(matches!(variants[0].seeds[1], ManualSeed::DataField(_))); + } + + #[test] + fn test_constant_path_seeds() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [RECORD_SEED, ctx.owner])] + Record(MinimalRecord), + } + "#, + ); + + let variants = parse_enum_variants(&input).expect("should parse"); + assert!(matches!(variants[0].seeds[0], ManualSeed::Constant(_))); + } + + // ========================================================================= + // BUILDER TESTS: verify build_intermediate_types for each configuration + // ========================================================================= + + #[allow(clippy::type_complexity)] + fn parse_and_build( + input_str: &str, + ) -> ( + Vec, + Option>, + Option>, + Vec, + bool, + bool, + TokenStream, + ) { + let input = parse_derive_input(input_str); + let variants = parse_enum_variants(&input).expect("should parse"); + let crate_ctx = CrateContext::empty(); + build_intermediate_types(&variants, &crate_ctx).expect("should build") + } + + /// 1 PDA: verify compressible_accounts, pda_seeds, no token/mint/ata + #[test] + fn test_build_single_pda() { + let (accounts, pda_seeds, token_seeds, _instr_data, has_mint, has_ata, _variant_code) = + parse_and_build( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"minimal_record", ctx.owner])] + MinimalRecord(MinimalRecord), + } + "#, + ); + + assert_eq!(accounts.len(), 1, "should have 1 compressible account"); + assert!(!accounts[0].is_zero_copy, "should not be zero_copy"); + assert!(pda_seeds.is_some(), "should have pda_seeds"); + assert_eq!(pda_seeds.as_ref().unwrap().len(), 1); + assert_eq!( + pda_seeds.as_ref().unwrap()[0].variant.to_string(), + "MinimalRecord" + ); + assert!(token_seeds.is_none(), "should have no token_seeds"); + assert!(!has_mint, "should not have mint"); + assert!(!has_ata, "should not have ata"); + } + + /// 1 ATA: verify has_ata_fields=true, nothing else + #[test] + fn test_build_single_ata() { + let (accounts, pda_seeds, token_seeds, _instr_data, has_mint, has_ata, _variant_code) = + parse_and_build( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(associated_token)] + Ata, + } + "#, + ); + + assert!(accounts.is_empty(), "should have no compressible accounts"); + assert!(pda_seeds.is_none(), "should have no pda_seeds"); + assert!(token_seeds.is_none(), "should have no token_seeds"); + assert!(!has_mint, "should not have mint"); + assert!(has_ata, "should have ata"); + } + + /// 1 token PDA: verify token_seeds, no pda_seeds/mint/ata + #[test] + fn test_build_single_token_pda() { + let (accounts, pda_seeds, token_seeds, _instr_data, has_mint, has_ata, _variant_code) = + parse_and_build( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(token::seeds = [VAULT_SEED, ctx.mint], token::owner_seeds = [VAULT_AUTH_SEED])] + Vault, + } + "#, + ); + + assert!(accounts.is_empty(), "should have no compressible accounts"); + assert!(pda_seeds.is_none(), "should have no pda_seeds"); + assert!(token_seeds.is_some(), "should have token_seeds"); + assert_eq!(token_seeds.as_ref().unwrap().len(), 1); + let ts = &token_seeds.as_ref().unwrap()[0]; + assert_eq!(ts.variant.to_string(), "Vault"); + assert_eq!(ts.is_token, Some(true)); + assert!(ts.owner_seeds.is_some(), "should have owner_seeds"); + assert!(!has_mint, "should not have mint"); + assert!(!has_ata, "should not have ata"); + } + + /// 1 account loader (zero_copy PDA): verify is_zero_copy flag + #[test] + fn test_build_single_account_loader() { + let (accounts, pda_seeds, token_seeds, _instr_data, has_mint, has_ata, _variant_code) = + parse_and_build( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [RECORD_SEED, ctx.owner], pda::zero_copy)] + ZeroCopyRecord(ZeroCopyRecord), + } + "#, + ); + + assert_eq!(accounts.len(), 1, "should have 1 compressible account"); + assert!(accounts[0].is_zero_copy, "should be zero_copy"); + assert!(pda_seeds.is_some(), "should have pda_seeds"); + assert_eq!(pda_seeds.as_ref().unwrap().len(), 1); + assert!( + pda_seeds.as_ref().unwrap()[0].is_zero_copy, + "seed spec should be zero_copy" + ); + assert!(token_seeds.is_none(), "should have no token_seeds"); + assert!(!has_mint, "should not have mint"); + assert!(!has_ata, "should not have ata"); + } + + /// Combined: 1 pda + 1 ata + 1 token pda + 1 account loader + #[test] + fn test_parse_full_combined() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"minimal_record", ctx.owner])] + MinimalRecord(MinimalRecord), + + #[light_account(associated_token)] + Ata, + + #[light_account(token::seeds = [VAULT_SEED, ctx.mint], token::owner_seeds = [VAULT_AUTH_SEED])] + Vault, + + #[light_account(pda::seeds = [RECORD_SEED, ctx.owner], pda::zero_copy)] + ZeroCopyRecord(ZeroCopyRecord), + } + "#, + ); + + let variants = parse_enum_variants(&input).expect("should parse"); + assert_eq!(variants.len(), 4); + + assert!(matches!(variants[0].kind, ManualVariantKind::Pda)); + assert!(!variants[0].is_zero_copy); + + assert!(matches!(variants[1].kind, ManualVariantKind::Ata)); + + assert!(matches!(variants[2].kind, ManualVariantKind::Token)); + assert!(variants[2].owner_seeds.is_some()); + + assert!(matches!(variants[3].kind, ManualVariantKind::Pda)); + assert!(variants[3].is_zero_copy); + } + + #[test] + fn test_build_full_combined() { + let (accounts, pda_seeds, token_seeds, _instr_data, has_mint, has_ata, _variant_code) = + parse_and_build( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"minimal_record", ctx.owner])] + MinimalRecord(MinimalRecord), + + #[light_account(associated_token)] + Ata, + + #[light_account(token::seeds = [VAULT_SEED, ctx.mint], token::owner_seeds = [VAULT_AUTH_SEED])] + Vault, + + #[light_account(pda::seeds = [RECORD_SEED, ctx.owner], pda::zero_copy)] + ZeroCopyRecord(ZeroCopyRecord), + } + "#, + ); + + // 2 PDA variants (one regular, one zero_copy) + assert_eq!(accounts.len(), 2, "should have 2 compressible accounts"); + assert!(!accounts[0].is_zero_copy, "first account is regular PDA"); + assert!(accounts[1].is_zero_copy, "second account is zero_copy"); + + // PDA seeds + assert!(pda_seeds.is_some(), "should have pda_seeds"); + let pda = pda_seeds.as_ref().unwrap(); + assert_eq!(pda.len(), 2, "should have 2 pda seed specs"); + assert_eq!(pda[0].variant.to_string(), "MinimalRecord"); + assert!(!pda[0].is_zero_copy); + assert_eq!(pda[1].variant.to_string(), "ZeroCopyRecord"); + assert!(pda[1].is_zero_copy); + + // Token seeds + assert!(token_seeds.is_some(), "should have token_seeds"); + let tok = token_seeds.as_ref().unwrap(); + assert_eq!(tok.len(), 1, "should have 1 token seed spec"); + assert_eq!(tok[0].variant.to_string(), "Vault"); + assert_eq!(tok[0].is_token, Some(true)); + assert!(tok[0].owner_seeds.is_some()); + + // Flags + assert!(!has_mint, "should not have mint"); + assert!(has_ata, "should have ata"); + } + + // ========================================================================= + // SEED ELEMENT CONVERSION TESTS + // ========================================================================= + + #[test] + fn test_seed_element_conversions() { + // ByteLiteral -> SeedElement::Expression + let byte_seed = ManualSeed::ByteLiteral(syn::parse_quote!(b"test")); + let elem = manual_seed_to_seed_element(&byte_seed); + assert!( + matches!(elem, SeedElement::Expression(_)), + "byte literal -> Expression" + ); + + // StrLiteral -> SeedElement::Literal + let str_seed = ManualSeed::StrLiteral(syn::parse_quote!("test")); + let elem = manual_seed_to_seed_element(&str_seed); + assert!( + matches!(elem, SeedElement::Literal(_)), + "str literal -> Literal" + ); + + // Constant -> SeedElement::Expression + let const_seed = ManualSeed::Constant(syn::parse_quote!(MY_CONSTANT)); + let elem = manual_seed_to_seed_element(&const_seed); + assert!( + matches!(elem, SeedElement::Expression(_)), + "constant -> Expression" + ); + + // CtxField -> SeedElement::Expression + let ctx_seed = ManualSeed::CtxField(format_ident!("owner")); + let elem = manual_seed_to_seed_element(&ctx_seed); + assert!( + matches!(elem, SeedElement::Expression(_)), + "ctx field -> Expression" + ); + + // DataField -> SeedElement::Expression + let data_seed = ManualSeed::DataField(format_ident!("key")); + let elem = manual_seed_to_seed_element(&data_seed); + assert!( + matches!(elem, SeedElement::Expression(_)), + "data field -> Expression" + ); + } + + #[test] + fn test_manual_seeds_to_punctuated() { + let seeds = vec![ + ManualSeed::ByteLiteral(syn::parse_quote!(b"prefix")), + ManualSeed::CtxField(format_ident!("owner")), + ManualSeed::Constant(syn::parse_quote!(EXTRA_SEED)), + ]; + + let punctuated = manual_seeds_to_punctuated(&seeds); + assert_eq!(punctuated.len(), 3); + } + + // ========================================================================= + // ERROR CASE TESTS + // ========================================================================= + + /// Token variant without seeds should fail + #[test] + fn test_error_token_missing_seeds() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(token::owner_seeds = [AUTH_SEED])] + Vault, + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("token::seeds"), "Error: {}", err_msg); + } + + /// Token variant without owner_seeds should fail + #[test] + fn test_error_token_missing_owner_seeds() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(token::seeds = [VAULT_SEED])] + Vault, + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("token::owner_seeds"), "Error: {}", err_msg); + } + + /// PDA variant with unit type (no tuple field) should fail + #[test] + fn test_error_pda_unit_variant() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"test"])] + Record, + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + } + + /// Token variant with fields should fail + #[test] + fn test_error_token_with_fields() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(token::seeds = [SEED], token::owner_seeds = [AUTH])] + Vault(SomeType), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("unit variant"), "Error: {}", err_msg); + } + + /// ATA variant with fields should fail + #[test] + fn test_error_ata_with_fields() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(associated_token)] + Ata(SomeType), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + } + + /// Standalone mint keyword should fail (mints handled by Light Token Program) + #[test] + fn test_error_mint_keyword_rejected() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(mint)] + MintAccount, + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Unknown keyword") || err_msg.contains("Light Token Program"), + "Error: {}", + err_msg + ); + } + + /// Unknown keyword should fail + #[test] + fn test_error_unknown_keyword() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(unknown)] + Something, + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Unknown keyword"), "Error: {}", err_msg); + } + + /// Derive on struct (not enum) should fail + #[test] + fn test_error_struct_not_enum() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub struct NotAnEnum { + pub field: u64, + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("can only be applied to enums"), + "Error: {}", + err_msg + ); + } + + /// Empty enum should parse but derive_light_program_impl should fail + #[test] + fn test_error_empty_enum() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts {} + "#, + ); + + let variants = parse_enum_variants(&input).expect("empty enum parses"); + assert!(variants.is_empty()); + } + + // ========================================================================= + // NAMESPACE VALIDATION TESTS + // ========================================================================= + + /// Mixed namespaces on same variant should fail + #[test] + fn test_error_mixed_namespaces() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"test"], token::seeds = [SEED])] + Mixed(SomeType), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Mixed namespaces"), "Error: {}", err_msg); + } + + /// Unknown namespace should fail + #[test] + fn test_error_unknown_namespace() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(foo::seeds = [b"test"])] + Something(SomeType), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Unknown namespace") || err_msg.contains("foo"), + "Error: {}", + err_msg + ); + } + + /// Unknown key within valid namespace should fail + #[test] + fn test_error_unknown_key_in_namespace() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::invalid = [b"test"])] + Something(SomeType), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("invalid") && err_msg.contains("pda"), + "Error: {}", + err_msg + ); + } + + /// Duplicate keys should fail + #[test] + fn test_error_duplicate_key() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"a"], pda::seeds = [b"b"])] + Something(SomeType), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!(err_msg.contains("Duplicate"), "Error: {}", err_msg); + } + + /// Bare seeds keyword (without namespace) should fail + #[test] + fn test_error_bare_seeds_keyword() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(seeds = [b"test"])] + Something(SomeType), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Unknown keyword") || err_msg.contains("seeds"), + "Error: {}", + err_msg + ); + } + + /// Bare pda keyword (old syntax) should fail + #[test] + fn test_error_bare_pda_keyword() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda, seeds = [b"test"])] + Something(SomeType), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("pda") + && (err_msg.contains("Unknown keyword") || err_msg.contains("namespaced")), + "Error: {}", + err_msg + ); + } + + /// associated_token standalone keyword works + #[test] + fn test_associated_token_keyword() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(associated_token)] + Ata, + } + "#, + ); + + let variants = parse_enum_variants(&input).expect("should parse"); + assert_eq!(variants.len(), 1); + assert!(matches!(variants[0].kind, ManualVariantKind::Ata)); + } + + /// Bare keyword in middle position should fail + #[test] + fn test_error_bare_keyword_in_middle() { + let input = parse_derive_input( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"test"], zero_copy)] + Something(SomeType), + } + "#, + ); + + let result = parse_enum_variants(&input); + assert!(result.is_err()); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("Unexpected keyword") || err_msg.contains("namespaced"), + "Error: {}", + err_msg + ); + } + + // ========================================================================= + // DATA FIELD EXTRACTION TESTS + // ========================================================================= + + /// PDA with data.field seeds should generate InstructionDataSpecs + #[test] + fn test_build_data_field_extraction() { + let (_, _, _, instr_data, _, _, _) = parse_and_build( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"record", data.some_key, data.another_key])] + Record(MinimalRecord), + } + "#, + ); + + assert_eq!(instr_data.len(), 2, "should extract 2 data fields"); + let names: Vec = instr_data + .iter() + .map(|s| s.field_name.to_string()) + .collect(); + assert!(names.contains(&"some_key".to_string())); + assert!(names.contains(&"another_key".to_string())); + } + + /// Duplicate data fields across variants should be deduplicated + #[test] + fn test_build_dedup_data_fields() { + let (_, _, _, instr_data, _, _, _) = parse_and_build( + r#" + #[derive(LightProgram)] + pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"record_a", data.owner])] + RecordA(RecordA), + + #[light_account(pda::seeds = [b"record_b", data.owner])] + RecordB(RecordB), + } + "#, + ); + + assert_eq!( + instr_data.len(), + 1, + "duplicate data.owner should be deduplicated" + ); + assert_eq!(instr_data[0].field_name.to_string(), "owner"); + } +} diff --git a/sdk-libs/macros/src/light_pdas/program/instructions.rs b/sdk-libs/macros/src/light_pdas/program/instructions.rs index 428a26b341..a67d26bf72 100644 --- a/sdk-libs/macros/src/light_pdas/program/instructions.rs +++ b/sdk-libs/macros/src/light_pdas/program/instructions.rs @@ -4,19 +4,23 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Item, ItemMod, Result}; -// Re-export types from parsing for external use -pub use super::parsing::{ - extract_ctx_seed_fields, extract_data_seed_fields, InstructionDataSpec, InstructionVariant, - SeedElement, TokenSeedSpec, -}; +// Re-export types from parsing, compress, and variant_enum for external use +pub use super::compress::CompressibleAccountInfo; use super::{ - compress::{CompressBuilder, CompressibleAccountInfo}, + compress::CompressBuilder, decompress::DecompressBuilder, parsing::{ convert_classified_to_seed_elements, convert_classified_to_seed_elements_vec, extract_context_and_params, macro_error, wrap_function_with_light, }, - variant_enum::{LightVariantBuilder, PdaCtxSeedInfo}, + variant_enum::LightVariantBuilder, +}; +pub use super::{ + parsing::{ + extract_ctx_seed_fields, extract_data_seed_fields, InstructionDataSpec, InstructionVariant, + SeedElement, TokenSeedSpec, + }, + variant_enum::PdaCtxSeedInfo, }; use crate::{ light_pdas::shared_utils::{ident_to_type, qualify_type_with_crate}, @@ -27,11 +31,13 @@ use crate::{ // MAIN CODEGEN // ============================================================================= -/// Orchestrates all code generation for the rentfree module. +/// Shared code generation used by both `#[light_program]` and `#[derive(LightProgram)]`. +/// +/// Returns a `Vec` of all generated items (enums, structs, trait impls, +/// instruction handlers, etc.) that can be injected into a module or returned directly. #[inline(never)] #[allow(clippy::too_many_arguments)] -fn codegen( - module: &mut ItemMod, +pub(crate) fn generate_light_program_items( compressible_accounts: Vec, pda_seeds: Option>, token_seeds: Option>, @@ -40,20 +46,8 @@ fn codegen( has_mint_fields: bool, has_ata_fields: bool, pda_variant_code: TokenStream, -) -> Result { - let content = match module.content.as_mut() { - Some(content) => content, - None => return Err(macro_error!(module, "Module must have a body")), - }; - - // Insert anchor_lang::prelude::* import at the beginning of the module - // This ensures Accounts, Signer, AccountInfo, Result, error_code etc. are in scope - // for the generated code (structs, enums, functions). - let anchor_import: syn::Item = syn::parse_quote! { - use anchor_lang::prelude::*; - }; - content.1.insert(0, anchor_import); - + enum_name: Option<&syn::Ident>, +) -> Result> { // TODO: Unify seed extraction - currently #[light_program] extracts seeds from Anchor's // #[account(seeds = [...])] automatically, while #[derive(LightAccounts)] requires // explicit token::seeds = [...] in #[light_account]. Consider removing the duplicate @@ -133,55 +127,55 @@ fn codegen( } } - impl ::light_sdk::hasher::DataHasher for LightAccountVariant { - fn hash(&self) -> std::result::Result<[u8; 32], ::light_sdk::hasher::HasherError> { + impl light_account::hasher::DataHasher for LightAccountVariant { + fn hash(&self) -> std::result::Result<[u8; 32], light_account::hasher::HasherError> { match self { - Self::Empty => Err(::light_sdk::hasher::HasherError::EmptyInput), + Self::Empty => Err(light_account::hasher::HasherError::EmptyInput), } } } - impl light_sdk::LightDiscriminator for LightAccountVariant { + impl light_account::LightDiscriminator for LightAccountVariant { const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; } - impl light_sdk::interface::HasCompressionInfo for LightAccountVariant { - fn compression_info(&self) -> std::result::Result<&light_sdk::interface::CompressionInfo, solana_program_error::ProgramError> { - Err(solana_program_error::ProgramError::InvalidAccountData) + impl light_account::HasCompressionInfo for LightAccountVariant { + fn compression_info(&self) -> std::result::Result<&light_account::CompressionInfo, light_account::LightSdkTypesError> { + Err(light_account::LightSdkTypesError::InvalidInstructionData) } - fn compression_info_mut(&mut self) -> std::result::Result<&mut light_sdk::interface::CompressionInfo, solana_program_error::ProgramError> { - Err(solana_program_error::ProgramError::InvalidAccountData) + fn compression_info_mut(&mut self) -> std::result::Result<&mut light_account::CompressionInfo, light_account::LightSdkTypesError> { + Err(light_account::LightSdkTypesError::InvalidInstructionData) } - fn compression_info_mut_opt(&mut self) -> &mut Option { + fn compression_info_mut_opt(&mut self) -> &mut Option { panic!("compression_info_mut_opt not supported for mint-only programs") } - fn set_compression_info_none(&mut self) -> std::result::Result<(), solana_program_error::ProgramError> { - Err(solana_program_error::ProgramError::InvalidAccountData) + fn set_compression_info_none(&mut self) -> std::result::Result<(), light_account::LightSdkTypesError> { + Err(light_account::LightSdkTypesError::InvalidInstructionData) } } - impl light_sdk::account::Size for LightAccountVariant { - fn size(&self) -> std::result::Result { - Err(solana_program_error::ProgramError::InvalidAccountData) + impl light_account::Size for LightAccountVariant { + fn size(&self) -> std::result::Result { + Err(light_account::LightSdkTypesError::InvalidInstructionData) } } // Pack trait is only available off-chain (client-side) #[cfg(not(target_os = "solana"))] - impl light_sdk::Pack for LightAccountVariant { + impl light_account::Pack for LightAccountVariant { type Packed = Self; - fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> std::result::Result { + fn pack(&self, _remaining_accounts: &mut light_account::interface::instruction::PackedAccounts) -> std::result::Result { Ok(Self::Empty) } } - impl light_sdk::Unpack for LightAccountVariant { + impl light_account::Unpack for LightAccountVariant { type Unpacked = Self; - fn unpack(&self, _remaining_accounts: &[solana_account_info::AccountInfo]) -> std::result::Result { + fn unpack(&self, _remaining_accounts: &[AI]) -> std::result::Result { Ok(Self::Empty) } } @@ -189,14 +183,14 @@ fn codegen( /// Wrapper for compressed account data (mint-only placeholder). #[derive(Clone, Debug, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] pub struct LightAccountData { - pub meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + pub meta: light_account::account_meta::CompressedAccountMetaNoLamportsNoAddress, pub data: LightAccountVariant, } impl Default for LightAccountData { fn default() -> Self { Self { - meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress::default(), + meta: light_account::account_meta::CompressedAccountMetaNoLamportsNoAddress::default(), data: LightAccountVariant::default(), } } @@ -335,9 +329,10 @@ fn codegen( }) } } - impl light_sdk::interface::IntoVariant for #seeds_struct_name { - fn into_variant(self, data: &[u8]) -> std::result::Result { + impl light_account::IntoVariant for #seeds_struct_name { + fn into_variant(self, data: &[u8]) -> std::result::Result { LightAccountVariant::#constructor_name(data, self) + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData) } } }; @@ -358,8 +353,8 @@ fn codegen( (false, false, true, _) => InstructionVariant::MintOnly, (false, false, false, true) => InstructionVariant::AtaOnly, (false, false, false, false) => { - return Err(macro_error!( - module, + return Err(syn::Error::new( + proc_macro2::Span::call_site(), "No #[light_account(init)], #[light_account(init, mint::...)], #[light_account(init, associated_token::...)], or #[light_account(token::...)] fields found.\n\ At least one light account field must be provided." )) @@ -374,11 +369,15 @@ fn codegen( let error_codes = compress_builder.generate_error_codes()?; // Create DecompressBuilder to generate all decompress-related code - let decompress_builder = DecompressBuilder::new(pda_ctx_seeds.clone(), pda_seeds.clone()); + let decompress_builder = DecompressBuilder::new( + pda_ctx_seeds.clone(), + pda_seeds.clone(), + has_token_seeds_early, + ); // Note: DecompressBuilder validation is optional for now since pda_seeds may be empty for TokenOnly let decompress_accounts = decompress_builder.generate_accounts_struct()?; - let pda_seed_provider_impls = decompress_builder.generate_seed_provider_impls()?; + let pda_seed_provider_impls = decompress_builder.generate_seed_provider_impls(false)?; // Generate trait impls and decompress processor/instruction based on program type. // v2 interface: no DecompressContext trait needed - uses DecompressVariant on PackedLightAccountVariant. @@ -401,7 +400,7 @@ fn codegen( mod __trait_impls { use super::*; - impl light_sdk::interface::HasTokenVariant for LightAccountData { + impl light_account::HasTokenVariant for LightAccountData { fn is_packed_token(&self) -> bool { match &self.data { #(#token_match_arms)* @@ -424,7 +423,7 @@ fn codegen( mod __trait_impls { use super::*; - impl light_sdk::interface::HasTokenVariant for LightAccountData { + impl light_account::HasTokenVariant for LightAccountData { fn is_packed_token(&self) -> bool { // PDA-only programs have no token variants false @@ -445,7 +444,7 @@ fn codegen( mod __trait_impls { use super::*; - impl light_sdk::interface::HasTokenVariant for LightAccountData { + impl light_account::HasTokenVariant for LightAccountData { fn is_packed_token(&self) -> bool { match &self.data { LightAccountVariant::Empty => false, @@ -511,58 +510,57 @@ fn codegen( } }; + let init_config_params_struct: syn::ItemStruct = syn::parse_quote! { + /// Configuration parameters for initializing compression config. + /// Field order matches SDK client's `InitializeCompressionConfigAnchorData`. + #[derive(AnchorSerialize, AnchorDeserialize, Clone)] + pub struct InitConfigParams { + pub write_top_up: u32, + pub rent_sponsor: Pubkey, + pub compression_authority: Pubkey, + pub rent_config: light_account::RentConfig, + pub address_space: Vec, + } + }; + 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_sdk::interface::rent::RentConfig, - address_space: Vec, + params: InitConfigParams, ) -> Result<()> { - light_sdk::interface::process_initialize_light_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, - )?; + light_account::process_initialize_light_config( + &ctx.accounts.config, + &ctx.accounts.authority, + ¶ms.rent_sponsor.to_bytes(), + ¶ms.compression_authority.to_bytes(), + params.rent_config, + params.write_top_up, + params.address_space.iter().map(|p| p.to_bytes()).collect(), + 0, // config_bump + &ctx.accounts.payer, + &ctx.accounts.system_program, + &crate::LIGHT_CPI_SIGNER.program_id, + ).map_err(|e| anchor_lang::error::Error::from(solana_program_error::ProgramError::from(e)))?; 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<::light_sdk::interface::rent::RentConfig>, - new_write_top_up: Option, - new_address_space: Option>, - new_update_authority: Option, + instruction_data: Vec, ) -> Result<()> { - light_sdk::interface::process_update_light_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, - )?; + let remaining = [ + ctx.accounts.config.to_account_info(), + ctx.accounts.update_authority.to_account_info(), + ]; + light_account::process_update_light_config( + &remaining, + &instruction_data, + &crate::LIGHT_CPI_SIGNER.program_id, + ).map_err(|e| anchor_lang::error::Error::from(solana_program_error::ProgramError::from(e)))?; Ok(()) } }; @@ -573,81 +571,125 @@ fn codegen( &instruction_data, )?; - // Insert SeedParams struct and impl - let seed_params_file: syn::File = syn::parse2(seed_params_struct)?; - for item in seed_params_file.items { - content.1.push(item); - } + // Collect all generated items into a Vec + let mut items: Vec = Vec::new(); - // Insert XxxSeeds structs and LightAccountVariant constructors + // SeedParams struct and impl + items.push(seed_params_struct); + + // XxxSeeds structs and LightAccountVariant 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); - } + items.push(seeds_tokens); } - // Insert PDA variant structs directly into the module. - // The variant code uses fully qualified paths (crate::CONSTANT) for all - // constant references, so no additional imports are needed. + // PDA variant structs (variant code uses fully qualified paths) if !pda_variant_code.is_empty() { - let wrapped: syn::File = syn::parse2(pda_variant_code)?; - for item in wrapped.items { - content.1.push(item); - } + items.push(pda_variant_code); } - content.1.push(Item::Verbatim(size_validation_checks)); - content.1.push(Item::Verbatim(enum_and_traits)); - content.1.push(Item::Struct(decompress_accounts)); - content.1.push(Item::Verbatim( - decompress_builder.generate_accounts_trait_impls()?, - )); + items.push(size_validation_checks); + items.push(enum_and_traits); + items.push(quote! { #decompress_accounts }); + items.push(decompress_builder.generate_accounts_trait_impls()?); if let Some(trait_impls) = trait_impls { - content.1.push(Item::Mod(trait_impls)); + items.push(quote! { #trait_impls }); } - content.1.push(Item::Mod(processor_module)); + items.push(quote! { #processor_module }); if let Some(decompress_instruction) = decompress_instruction { - content.1.push(Item::Fn(decompress_instruction)); + items.push(quote! { #decompress_instruction }); } - content.1.push(Item::Struct(compress_accounts)); - content.1.push(Item::Verbatim( - compress_builder.generate_accounts_trait_impls()?, - )); - 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 + items.push(quote! { #compress_accounts }); + items.push(compress_builder.generate_accounts_trait_impls()?); + items.push(quote! { #compress_instruction }); + items.push(quote! { #init_config_accounts }); + items.push(quote! { #update_config_accounts }); + items.push(quote! { #init_config_params_struct }); + items.push(quote! { #init_config_instruction }); + items.push(quote! { #update_config_instruction }); + + // 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); - } + items.push(pda_impl); } - // Add ctoken seed provider impls (one per token variant) + // CToken seed provider impls (one per token variant) if let Some(ref seeds) = token_seeds { if !seeds.is_empty() { let impl_code = super::seed_codegen::generate_ctoken_seed_provider_implementation(seeds)?; - let impl_file: syn::File = syn::parse2(impl_code)?; - for item in impl_file.items { - content.1.push(item); - } + items.push(impl_code); } } - // Add error codes - let error_item: syn::ItemEnum = syn::parse2(error_codes)?; - content.1.push(Item::Enum(error_item)); + // Error codes + items.push(error_codes); + + // Client functions (module + pub use statement) + items.push(client_functions); + + // Generate enum dispatch methods for #[derive(LightProgram)] + if let Some(enum_name) = enum_name { + // Compress dispatch: impl EnumName { pub fn compress_dispatch(...) } + if compress_builder.has_pdas() { + items.push(compress_builder.generate_enum_dispatch_method(enum_name)?); + } + + // Decompress dispatch: impl EnumName { pub fn decompress_dispatch(...) } + if !pda_ctx_seeds.is_empty() { + items.push(decompress_builder.generate_enum_decompress_dispatch(enum_name)?); + } + } + + Ok(items) +} + +/// Thin wrapper around `generate_light_program_items` that injects items into a module. +/// +/// Used by `#[light_program]` attribute macro. +#[inline(never)] +#[allow(clippy::too_many_arguments)] +fn codegen( + module: &mut ItemMod, + compressible_accounts: Vec, + pda_seeds: Option>, + token_seeds: Option>, + instruction_data: Vec, + crate_ctx: &crate::light_pdas::parsing::CrateContext, + has_mint_fields: bool, + has_ata_fields: bool, + pda_variant_code: TokenStream, +) -> Result { + let content = match module.content.as_mut() { + Some(content) => content, + None => return Err(macro_error!(module, "Module must have a body")), + }; + + // Insert anchor_lang::prelude::* import at the beginning of the module + let anchor_import: syn::Item = syn::parse_quote! { + use anchor_lang::prelude::*; + }; + content.1.insert(0, anchor_import); + + // Generate all items using the shared function + // #[light_program] attribute macro doesn't have an enum name - pass None + let generated_items = generate_light_program_items( + compressible_accounts, + pda_seeds, + token_seeds, + instruction_data, + crate_ctx, + has_mint_fields, + has_ata_fields, + pda_variant_code, + None, + )?; - // 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); + // Inject all generated items into the module + for item_tokens in generated_items { + let file: syn::File = syn::parse2(item_tokens)?; + for item in file.items { + content.1.push(item); + } } Ok(quote! { #module }) @@ -910,3 +952,471 @@ pub fn light_program_impl(_args: TokenStream, mut module: ItemMod) -> Result, + pda_seeds: Option>, + token_seeds: Option>, + instruction_data: Vec, + crate_ctx: &crate::light_pdas::parsing::CrateContext, + has_mint_fields: bool, + has_ata_fields: bool, + pda_variant_code: TokenStream, + enum_name: Option<&syn::Ident>, +) -> Result> { + // Validate token seeds have seeds specified + if let Some(ref token_seed_specs) = token_seeds { + for spec in token_seed_specs { + if spec.seeds.is_empty() { + return Err(super::parsing::macro_error!( + &spec.variant, + "Token account '{}' must have seeds in #[account(seeds = [...])] for PDA signing.", + spec.variant + )); + } + } + } + + // Build PDA context seed info (same logic as Anchor version) + let pda_ctx_seeds: Vec = pda_seeds + .as_ref() + .map(|specs| { + specs + .iter() + .map(|spec| { + let ctx_fields = extract_ctx_seed_fields(&spec.seeds); + let inner_type = spec + .inner_type + .clone() + .unwrap_or_else(|| ident_to_type(&spec.variant)); + + let state_field_names: std::collections::HashSet = crate_ctx + .get_struct_fields(&inner_type) + .map(|fields| fields.into_iter().collect()) + .unwrap_or_default(); + + let params_only_seed_fields = + crate::light_pdas::seeds::get_params_only_seed_fields_from_spec( + spec, + &state_field_names, + ); + + let seed_count = spec.seeds.len() + 1; + + PdaCtxSeedInfo::with_state_fields( + spec.variant.clone(), + inner_type, + ctx_fields, + state_field_names, + params_only_seed_fields, + seed_count, + ) + }) + .collect() + }) + .unwrap_or_default(); + + let has_token_seeds_early = token_seeds.as_ref().map(|t| !t.is_empty()).unwrap_or(false); + + // Generate variant enum and traits using pinocchio builder + let enum_and_traits = if pda_ctx_seeds.is_empty() { + // Minimal placeholder for programs without PDA state accounts + quote! { + #[derive(Clone, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)] + pub enum LightAccountVariant { + Empty, + } + + impl Default for LightAccountVariant { + fn default() -> Self { + Self::Empty + } + } + + impl light_account_pinocchio::hasher::DataHasher for LightAccountVariant { + fn hash(&self) -> std::result::Result<[u8; 32], light_account_pinocchio::hasher::HasherError> { + match self { + Self::Empty => Err(light_account_pinocchio::hasher::HasherError::EmptyInput), + } + } + } + + impl light_account_pinocchio::LightDiscriminator for LightAccountVariant { + const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; + } + + impl light_account_pinocchio::HasCompressionInfo for LightAccountVariant { + fn compression_info(&self) -> std::result::Result<&light_account_pinocchio::CompressionInfo, light_account_pinocchio::LightSdkTypesError> { + Err(light_account_pinocchio::LightSdkTypesError::InvalidInstructionData) + } + + fn compression_info_mut(&mut self) -> std::result::Result<&mut light_account_pinocchio::CompressionInfo, light_account_pinocchio::LightSdkTypesError> { + Err(light_account_pinocchio::LightSdkTypesError::InvalidInstructionData) + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + panic!("compression_info_mut_opt not supported for mint-only programs") + } + + fn set_compression_info_none(&mut self) -> std::result::Result<(), light_account_pinocchio::LightSdkTypesError> { + Err(light_account_pinocchio::LightSdkTypesError::InvalidInstructionData) + } + } + + impl light_account_pinocchio::Size for LightAccountVariant { + fn size(&self) -> std::result::Result { + Err(light_account_pinocchio::LightSdkTypesError::InvalidInstructionData) + } + } + + #[cfg(not(target_os = "solana"))] + impl light_account_pinocchio::Pack for LightAccountVariant { + type Packed = Self; + fn pack(&self, _remaining_accounts: &mut light_account_pinocchio::interface::instruction::PackedAccounts) -> std::result::Result { + Ok(Self::Empty) + } + } + + impl light_account_pinocchio::Unpack for LightAccountVariant { + type Unpacked = Self; + fn unpack(&self, _remaining_accounts: &[AI]) -> std::result::Result { + Ok(Self::Empty) + } + } + + #[derive(Clone, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)] + pub struct LightAccountData { + pub meta: light_account_pinocchio::account_meta::CompressedAccountMetaNoLamportsNoAddress, + pub data: LightAccountVariant, + } + + impl Default for LightAccountData { + fn default() -> Self { + Self { + meta: light_account_pinocchio::account_meta::CompressedAccountMetaNoLamportsNoAddress::default(), + data: LightAccountVariant::default(), + } + } + } + } + } else { + let builder = LightVariantBuilder::new(&pda_ctx_seeds); + let builder = if let Some(ref token_seed_specs) = token_seeds { + if !token_seed_specs.is_empty() { + builder.with_token_seeds(token_seed_specs) + } else { + builder + } + } else { + builder + }; + builder.build_pinocchio()? + }; + + // Collect params-only seed fields for SeedParams struct + let mut all_params_only_fields: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + for ctx_info in &pda_ctx_seeds { + for (field_name, field_type, _) in &ctx_info.params_only_seed_fields { + let field_str = field_name.to_string(); + all_params_only_fields + .entry(field_str) + .or_insert_with(|| field_type.clone()); + } + } + + // SeedParams with Borsh derives instead of Anchor derives + let seed_params_struct = if all_params_only_fields.is_empty() { + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug, Default)] + pub struct SeedParams; + } + } else { + let sorted_fields: Vec<_> = all_params_only_fields.iter().collect(); + let seed_param_fields: Vec<_> = sorted_fields + .iter() + .map(|(name, ty)| { + let field_ident = format_ident!("{}", name); + quote! { pub #field_ident: Option<#ty> } + }) + .collect(); + let seed_param_defaults: Vec<_> = sorted_fields + .iter() + .map(|(name, _)| { + let field_ident = format_ident!("{}", name); + quote! { #field_ident: None } + }) + .collect(); + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub struct SeedParams { + #(#seed_param_fields,)* + } + impl Default for SeedParams { + fn default() -> Self { + Self { + #(#seed_param_defaults,)* + } + } + } + } + }; + + // Seeds constructors with BorshDeserialize and light_account_pinocchio errors + 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 variant_name = &ctx_info.variant_name; + let inner_type = qualify_type_with_crate(&ctx_info.inner_type); + let seeds_struct_name = format_ident!("{}Seeds", variant_name); + let constructor_name = + format_ident!("{}", to_snake_case(&variant_name.to_string())); + let data_fields = extract_data_seed_fields(&spec.seeds); + + let data_verifications: Vec<_> = data_fields.iter().filter_map(|field| { + let field_str = field.to_string(); + if !ctx_info.state_field_names.contains(&field_str) { + return None; + } + Some(quote! { + if data.#field != seeds.#field { + return std::result::Result::Err( + light_account_pinocchio::LightSdkTypesError::InvalidInstructionData + ); + } + }) + }).collect(); + + // Pinocchio: use BorshDeserialize with light_account_pinocchio errors + let (deserialize_code, variant_data) = ( + quote! { + use borsh::BorshDeserialize; + let data: #inner_type = BorshDeserialize::deserialize(&mut &account_data[..]) + .map_err(|_| light_account_pinocchio::LightSdkTypesError::Borsh)?; + }, + quote! { data }, + ); + + quote! { + impl LightAccountVariant { + pub fn #constructor_name( + account_data: &[u8], + seeds: #seeds_struct_name, + ) -> std::result::Result { + #deserialize_code + + #(#data_verifications)* + + std::result::Result::Ok(Self::#variant_name { + seeds, + data: #variant_data, + }) + } + } + impl light_account_pinocchio::IntoVariant for #seeds_struct_name { + fn into_variant(self, data: &[u8]) -> std::result::Result { + LightAccountVariant::#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, has_mint_fields, has_ata_fields) + { + (true, true, _, _) => InstructionVariant::Mixed, + (true, false, _, _) => InstructionVariant::PdaOnly, + (false, true, _, _) => InstructionVariant::TokenOnly, + (false, false, true, _) => InstructionVariant::MintOnly, + (false, false, false, true) => InstructionVariant::AtaOnly, + (false, false, false, false) => { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + "No #[light_account(init)], #[light_account(init, mint::...)], #[light_account(init, associated_token::...)], or #[light_account(token::...)] fields found.\n\ + At least one light account field must be provided.", + )) + } + }; + + // Create builders for compress/decompress + let compress_builder = CompressBuilder::new(compressible_accounts.clone(), instruction_variant); + compress_builder.validate()?; + + let size_validation_checks = compress_builder.generate_size_validation_pinocchio()?; + + let decompress_builder = DecompressBuilder::new( + pda_ctx_seeds.clone(), + pda_seeds.clone(), + has_token_seeds_early, + ); + + // PDA seed provider impls (framework-agnostic, reused as-is) + let pda_seed_provider_impls = decompress_builder.generate_seed_provider_impls(true)?; + + // InitConfigParams with [u8; 32] instead of Pubkey + let init_config_params_struct = quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone)] + pub struct InitConfigParams { + pub write_top_up: u32, + pub rent_sponsor: [u8; 32], + pub compression_authority: [u8; 32], + pub rent_config: light_compressible::rent::RentConfig, + pub address_space: Vec<[u8; 32]>, + } + }; + + // Client functions (module + pub use - framework-agnostic) + let client_functions = super::seed_codegen::generate_client_seed_functions( + &pda_seeds, + &token_seeds, + &instruction_data, + )?; + + // Collect all generated items + let mut items: Vec = Vec::new(); + + // SeedParams struct + items.push(seed_params_struct); + + // Seeds structs and constructors + for seeds_tokens in seeds_structs_and_constructors.into_iter() { + items.push(seeds_tokens); + } + + // PDA variant structs (already generated with pinocchio derives) + if !pda_variant_code.is_empty() { + items.push(pda_variant_code); + } + + // Size validation + items.push(size_validation_checks); + + // Variant enums and traits + items.push(enum_and_traits); + + // InitConfigParams + items.push(init_config_params_struct); + + // PDA seed provider impls + for pda_impl in pda_seed_provider_impls.into_iter() { + items.push(pda_impl); + } + + // CToken seed provider impls + if let Some(ref seeds) = token_seeds { + if !seeds.is_empty() { + let impl_code = + super::seed_codegen::generate_ctoken_seed_provider_implementation(seeds)?; + items.push(impl_code); + } + } + + // Client functions + items.push(client_functions); + + // Generate enum associated functions for pinocchio + if let Some(enum_name) = enum_name { + // Compress dispatch + process_compress + if compress_builder.has_pdas() { + items.push(compress_builder.generate_enum_dispatch_method_pinocchio(enum_name)?); + items.push(compress_builder.generate_enum_process_compress_pinocchio(enum_name)?); + } + + // Decompress dispatch + process_decompress + if !pda_ctx_seeds.is_empty() { + items.push(decompress_builder.generate_enum_process_decompress_pinocchio(enum_name)?); + } + + // Config functions as enum methods + items.push(quote! { + impl #enum_name { + // SDK-standard discriminators (must match light-client) + pub const INITIALIZE_COMPRESSION_CONFIG: [u8; 8] = [133, 228, 12, 169, 56, 76, 222, 61]; + pub const UPDATE_COMPRESSION_CONFIG: [u8; 8] = [135, 215, 243, 81, 163, 146, 33, 70]; + pub const COMPRESS_ACCOUNTS_IDEMPOTENT: [u8; 8] = [70, 236, 171, 120, 164, 93, 113, 181]; + pub const DECOMPRESS_ACCOUNTS_IDEMPOTENT: [u8; 8] = [114, 67, 61, 123, 234, 31, 1, 112]; + + pub fn process_initialize_config( + accounts: &[pinocchio::account_info::AccountInfo], + data: &[u8], + ) -> std::result::Result<(), pinocchio::program_error::ProgramError> { + let params = ::try_from_slice(data) + .map_err(|_| pinocchio::program_error::ProgramError::BorshIoError)?; + + if accounts.len() < 5 { + return Err(pinocchio::program_error::ProgramError::NotEnoughAccountKeys); + } + + let fee_payer = &accounts[0]; + let config = &accounts[1]; + let _program_data = &accounts[2]; + let authority = &accounts[3]; + let system_program = &accounts[4]; + + light_account_pinocchio::process_initialize_light_config( + config, + authority, + ¶ms.rent_sponsor, + ¶ms.compression_authority, + params.rent_config, + params.write_top_up, + params.address_space, + 0, // config_bump + fee_payer, + system_program, + &crate::LIGHT_CPI_SIGNER.program_id, + ) + .map_err(|e| pinocchio::program_error::ProgramError::Custom(u32::from(e))) + } + + pub fn process_update_config( + accounts: &[pinocchio::account_info::AccountInfo], + data: &[u8], + ) -> std::result::Result<(), pinocchio::program_error::ProgramError> { + if accounts.len() < 2 { + return Err(pinocchio::program_error::ProgramError::NotEnoughAccountKeys); + } + + let authority = &accounts[0]; + let config = &accounts[1]; + + let remaining = [*config, *authority]; + light_account_pinocchio::process_update_light_config( + &remaining, + data, + &crate::LIGHT_CPI_SIGNER.program_id, + ) + .map_err(|e| pinocchio::program_error::ProgramError::Custom(u32::from(e))) + } + } + }); + } + + Ok(items) +} diff --git a/sdk-libs/macros/src/light_pdas/program/mod.rs b/sdk-libs/macros/src/light_pdas/program/mod.rs index 24d5e1a708..cd6b58306b 100644 --- a/sdk-libs/macros/src/light_pdas/program/mod.rs +++ b/sdk-libs/macros/src/light_pdas/program/mod.rs @@ -5,8 +5,9 @@ //! - Auto-wraps instruction handlers with light_pre_init/light_finalize logic //! - Generates all necessary types, enums, and instruction handlers -mod compress; +pub(crate) mod compress; mod decompress; +pub mod derive_light_program; pub mod expr_traversal; pub mod instructions; pub mod seed_codegen; @@ -17,4 +18,5 @@ pub mod variant_enum; pub(crate) mod parsing; pub(crate) mod visitors; +pub use derive_light_program::{derive_light_program_impl, derive_light_program_pinocchio_impl}; pub use instructions::light_program_impl; diff --git a/sdk-libs/macros/src/light_pdas/program/parsing.rs b/sdk-libs/macros/src/light_pdas/program/parsing.rs index 0c8be23ee8..c740be621e 100644 --- a/sdk-libs/macros/src/light_pdas/program/parsing.rs +++ b/sdk-libs/macros/src/light_pdas/program/parsing.rs @@ -682,11 +682,9 @@ pub fn wrap_function_with_light( #(#fn_attrs)* #fn_vis #fn_sig { // Phase 1: Pre-init (creates mints via CPI context write, registers compressed addresses) - use light_sdk::interface::{LightPreInit, LightFinalize}; + use light_account::{LightPreInit, LightFinalize}; let _ = #ctx_name.accounts.light_pre_init(#ctx_name.remaining_accounts, &#params_ident) - .map_err(|e: light_sdk::error::LightSdkError| -> solana_program_error::ProgramError { - e.into() - })?; + .map_err(|e| anchor_lang::error::Error::from(solana_program_error::ProgramError::from(e)))?; // Execute delegation - this handles its own logic including any finalize #fn_block @@ -698,11 +696,9 @@ pub fn wrap_function_with_light( #(#fn_attrs)* #fn_vis #fn_sig { // Phase 1: Pre-init (creates mints via CPI context write, registers compressed addresses) - use light_sdk::interface::{LightPreInit, LightFinalize}; + use light_account::{LightPreInit, LightFinalize}; let __has_pre_init = #ctx_name.accounts.light_pre_init(#ctx_name.remaining_accounts, &#params_ident) - .map_err(|e: light_sdk::error::LightSdkError| -> solana_program_error::ProgramError { - e.into() - })?; + .map_err(|e| anchor_lang::error::Error::from(solana_program_error::ProgramError::from(e)))?; // Execute the original handler body and capture result let __user_result: anchor_lang::Result<()> = #fn_block; @@ -711,9 +707,7 @@ pub fn wrap_function_with_light( // Phase 2: Finalize (creates token accounts/ATAs via CPI) #ctx_name.accounts.light_finalize(#ctx_name.remaining_accounts, &#params_ident, __has_pre_init) - .map_err(|e: light_sdk::error::LightSdkError| -> solana_program_error::ProgramError { - e.into() - })?; + .map_err(|e| anchor_lang::error::Error::from(solana_program_error::ProgramError::from(e)))?; Ok(()) } diff --git a/sdk-libs/macros/src/light_pdas/program/seed_codegen.rs b/sdk-libs/macros/src/light_pdas/program/seed_codegen.rs index 7960ef3a4d..3a205812b1 100644 --- a/sdk-libs/macros/src/light_pdas/program/seed_codegen.rs +++ b/sdk-libs/macros/src/light_pdas/program/seed_codegen.rs @@ -42,7 +42,10 @@ pub fn generate_client_seed_functions( let (parameters, seed_expressions) = analyze_seed_spec_for_client(spec, instruction_data)?; - let fn_body = generate_seed_derivation_body(&seed_expressions, quote! { &crate::ID }); + let fn_body = generate_seed_derivation_body( + &seed_expressions, + quote! { &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id) }, + ); let function = quote! { pub fn #function_name(#(#parameters),*) -> (Vec>, solana_pubkey::Pubkey) { #fn_body @@ -62,7 +65,10 @@ pub fn generate_client_seed_functions( let (parameters, seed_expressions) = analyze_seed_spec_for_client(spec, instruction_data)?; - let fn_body = generate_seed_derivation_body(&seed_expressions, quote! { &crate::ID }); + let fn_body = generate_seed_derivation_body( + &seed_expressions, + quote! { &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id) }, + ); let function = quote! { pub fn #function_name(#(#parameters),*) -> (Vec>, solana_pubkey::Pubkey) { #fn_body @@ -106,7 +112,7 @@ pub fn generate_client_seed_functions( quote! { #(#owner_parameters),* }, generate_seed_derivation_body( &owner_seed_expressions, - quote! { &crate::ID }, + quote! { &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id) }, ), ) }; diff --git a/sdk-libs/macros/src/light_pdas/program/variant_enum.rs b/sdk-libs/macros/src/light_pdas/program/variant_enum.rs index 452d94aed4..2a71f9a0c2 100644 --- a/sdk-libs/macros/src/light_pdas/program/variant_enum.rs +++ b/sdk-libs/macros/src/light_pdas/program/variant_enum.rs @@ -97,6 +97,34 @@ impl<'a> LightVariantBuilder<'a> { }) } + /// Generate pinocchio-compatible enum definitions and trait implementations. + /// + /// Same as `build()` but uses: + /// - `BorshSerialize/BorshDeserialize` instead of `AnchorSerialize/AnchorDeserialize` + /// - `light_account_pinocchio::` instead of `light_account::` + /// - `pinocchio::account_info::AccountInfo` instead of anchor's AccountInfo + pub fn build_pinocchio(&self) -> Result { + self.validate()?; + + let token_seeds_structs = self.generate_token_seeds_structs_pinocchio(); + let token_variant_trait_impls = self.generate_token_variant_trait_impls_pinocchio(); + let unpacked_enum = self.generate_unpacked_enum_pinocchio(); + let packed_enum = self.generate_packed_enum_pinocchio(); + let light_account_data_struct = self.generate_light_account_data_struct_pinocchio(); + let decompress_variant_impl = self.generate_decompress_variant_impl_pinocchio(); + let pack_impl = self.generate_pack_impl_pinocchio(); + + Ok(quote! { + #token_seeds_structs + #token_variant_trait_impls + #unpacked_enum + #packed_enum + #light_account_data_struct + #decompress_variant_impl + #pack_impl + }) + } + /// Generate the `LightAccountData` wrapper struct. fn generate_light_account_data_struct(&self) -> TokenStream { quote! { @@ -104,7 +132,7 @@ impl<'a> LightVariantBuilder<'a> { /// Contains PACKED variant data that will be decompressed into PDA accounts. #[derive(Clone, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)] pub struct LightAccountData { - pub meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + pub meta: light_account::account_meta::CompressedAccountMetaNoLamportsNoAddress, pub data: PackedLightAccountVariant, } } @@ -146,7 +174,7 @@ impl<'a> LightVariantBuilder<'a> { .iter() .map(|f| { let idx = format_ident!("{}_idx", f); - quote! { #idx: remaining_accounts.insert_or_get(self.#f) } + quote! { #idx: remaining_accounts.insert_or_get(AM::pubkey_from_bytes(self.#f.to_bytes())) } }) .collect(); @@ -163,10 +191,12 @@ impl<'a> LightVariantBuilder<'a> { .map(|f| { let idx = format_ident!("{}_idx", f); quote! { - let #f = *remaining_accounts - .get(self.#idx as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? - .key; + let #f = solana_pubkey::Pubkey::new_from_array( + remaining_accounts + .get(self.#idx as usize) + .ok_or(light_account::LightSdkTypesError::InvalidInstructionData)? + .key() + ); } }) .collect(); @@ -198,17 +228,17 @@ impl<'a> LightVariantBuilder<'a> { // Pack trait is only available off-chain (client-side) #[cfg(not(target_os = "solana"))] - impl light_sdk::Pack for #seeds_name { + impl light_account::Pack for #seeds_name { type Packed = #packed_seeds_name; fn pack( &self, - remaining_accounts: &mut light_sdk::instruction::PackedAccounts, - ) -> std::result::Result { + remaining_accounts: &mut light_account::interface::instruction::PackedAccounts, + ) -> std::result::Result { let __seeds: &[&[u8]] = &[#(#bump_seed_refs),*]; let (_, __bump) = solana_pubkey::Pubkey::find_program_address( __seeds, - &crate::ID, + &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id), ); Ok(#packed_seeds_name { #(#pack_stmts,)* @@ -217,13 +247,13 @@ impl<'a> LightVariantBuilder<'a> { } } - impl light_sdk::Unpack for #packed_seeds_name { + impl light_account::Unpack for #packed_seeds_name { type Unpacked = #seeds_name; fn unpack( &self, - remaining_accounts: &[solana_account_info::AccountInfo], - ) -> std::result::Result { + remaining_accounts: &[AI], + ) -> std::result::Result { #(#unpack_resolve_stmts)* Ok(#seeds_name { #(#unpack_field_assigns,)* @@ -243,7 +273,7 @@ impl<'a> LightVariantBuilder<'a> { // ========================================================================= /// Generate `UnpackedTokenSeeds` and `PackedTokenSeeds` impls - /// on the local seed structs. The blanket impls in `light_sdk::interface::token` + /// on the local seed structs. The blanket impls in `light_account::token` /// then provide `LightAccountVariantTrait` / `PackedLightAccountVariantTrait`. fn generate_token_variant_trait_impls(&self) -> TokenStream { let impls: Vec<_> = self @@ -327,22 +357,22 @@ impl<'a> LightVariantBuilder<'a> { quote! { let (__owner, _) = solana_pubkey::Pubkey::find_program_address( &[#(#owner_seed_refs),*], - &crate::ID, + &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id), ); - __owner + __owner.to_bytes() } } else { // No owner_seeds - return default (shouldn't happen for token accounts) - quote! { solana_pubkey::Pubkey::default() } + quote! { [0u8; 32] } }; quote! { - impl light_sdk::interface::UnpackedTokenSeeds<#seed_count> + impl light_account::UnpackedTokenSeeds<#seed_count> for #seeds_name { type Packed = #packed_seeds_name; - const PROGRAM_ID: Pubkey = crate::ID; + const PROGRAM_ID: [u8; 32] = crate::LIGHT_CPI_SIGNER.program_id; fn seed_vec(&self) -> Vec> { vec![#(#seed_vec_items),*] @@ -353,23 +383,31 @@ impl<'a> LightVariantBuilder<'a> { } } - impl light_sdk::interface::PackedTokenSeeds<#seed_count> + impl light_account::PackedTokenSeeds<#seed_count> for #packed_seeds_name { + type Unpacked = #seeds_name; + fn bump(&self) -> u8 { self.bump } + fn unpack_seeds( + &self, + accounts: &[AI], + ) -> std::result::Result { + >::unpack(self, accounts) + } - fn seed_refs_with_bump<'a>( + fn seed_refs_with_bump<'a, AI: light_account::AccountInfoTrait>( &'a self, - accounts: &'a [anchor_lang::prelude::AccountInfo], + accounts: &'a [AI], bump_storage: &'a [u8; 1], - ) -> std::result::Result<[&'a [u8]; #seed_count], solana_program_error::ProgramError> { + ) -> std::result::Result<[&'a [u8]; #seed_count], light_account::LightSdkTypesError> { Ok([#(#packed_seed_ref_items,)* bump_storage]) } - fn derive_owner(&self) -> solana_pubkey::Pubkey { + fn derive_owner(&self) -> [u8; 32] { #owner_derivation } } @@ -404,7 +442,7 @@ impl<'a> LightVariantBuilder<'a> { let variant_name = &spec.variant; let seeds_name = format_ident!("{}Seeds", variant_name); quote! { - #variant_name(light_sdk::interface::token::TokenDataWithSeeds<#seeds_name>) + #variant_name(light_account::token::TokenDataWithSeeds<#seeds_name>) } }) .collect(); @@ -446,7 +484,7 @@ impl<'a> LightVariantBuilder<'a> { let variant_name = &spec.variant; let packed_seeds_name = format_ident!("Packed{}Seeds", variant_name); quote! { - #variant_name(light_sdk::interface::token::TokenDataWithPackedSeeds<#packed_seeds_name>) + #variant_name(light_account::token::TokenDataWithPackedSeeds<#packed_seeds_name>) } }) .collect(); @@ -478,7 +516,7 @@ impl<'a> LightVariantBuilder<'a> { quote! { Self::#variant_name { seeds, data } => { let packed_data = #packed_variant_type { seeds: seeds.clone(), data: data.clone() }; - light_sdk::interface::prepare_account_for_decompression::<#seed_count, #packed_variant_type>( + light_account::prepare_account_for_decompression::<#seed_count, #packed_variant_type, light_account::AccountInfo<'info>>( &packed_data, tree_info, output_queue_index, @@ -500,9 +538,10 @@ impl<'a> LightVariantBuilder<'a> { quote! { Self::#variant_name(packed_data) => { - light_sdk::interface::token::prepare_token_account_for_decompression::< + light_account::token::prepare_token_account_for_decompression::< #seed_count, - light_sdk::interface::token::TokenDataWithPackedSeeds<#packed_seeds_name>, + light_account::token::TokenDataWithPackedSeeds<#packed_seeds_name>, + light_account::AccountInfo<'info>, >( packed_data, tree_info, @@ -516,13 +555,13 @@ impl<'a> LightVariantBuilder<'a> { .collect(); quote! { - impl<'info> light_sdk::interface::DecompressVariant<'info> for PackedLightAccountVariant { + impl<'info> light_account::DecompressVariant> for PackedLightAccountVariant { fn decompress( &self, - tree_info: &light_sdk::instruction::PackedStateTreeInfo, - pda_account: &anchor_lang::prelude::AccountInfo<'info>, - ctx: &mut light_sdk::interface::DecompressCtx<'_, 'info>, - ) -> std::result::Result<(), solana_program_error::ProgramError> { + tree_info: &light_account::PackedStateTreeInfo, + pda_account: &light_account::AccountInfo<'info>, + ctx: &mut light_account::DecompressCtx<'_, 'info>, + ) -> std::result::Result<(), light_account::LightSdkTypesError> { let output_queue_index = ctx.output_queue_index; match self { #(#pda_arms)* @@ -537,7 +576,7 @@ impl<'a> LightVariantBuilder<'a> { // PACK IMPL // ========================================================================= - /// Generate `impl light_sdk::Pack for LightAccountVariant`. + /// Generate `impl light_account::Pack for LightAccountVariant`. fn generate_pack_impl(&self) -> TokenStream { let pda_arms: Vec<_> = self .pda_ctx_seeds @@ -549,7 +588,7 @@ impl<'a> LightVariantBuilder<'a> { quote! { Self::#variant_name { seeds, data } => { let variant = #variant_struct_name { seeds: seeds.clone(), data: data.clone() }; - let packed = light_sdk::Pack::pack(&variant, accounts)?; + let packed = light_account::Pack::pack(&variant, accounts)?; Ok(PackedLightAccountVariant::#variant_name { seeds: packed.seeds, data: packed.data }) } } @@ -563,7 +602,7 @@ impl<'a> LightVariantBuilder<'a> { let variant_name = &spec.variant; quote! { Self::#variant_name(data) => { - let packed = light_sdk::Pack::pack(data, accounts)?; + let packed = light_account::Pack::pack(data, accounts)?; Ok(PackedLightAccountVariant::#variant_name(packed)) } } @@ -573,13 +612,477 @@ impl<'a> LightVariantBuilder<'a> { quote! { // Pack trait is only available off-chain (client-side) #[cfg(not(target_os = "solana"))] - impl light_sdk::Pack for LightAccountVariant { + impl light_account::Pack for LightAccountVariant { type Packed = PackedLightAccountVariant; fn pack( &self, - accounts: &mut light_sdk::instruction::PackedAccounts, - ) -> std::result::Result { + accounts: &mut light_account::interface::instruction::PackedAccounts, + ) -> std::result::Result { + match self { + #(#pda_arms)* + #(#token_arms)* + } + } + } + } + } + + // ========================================================================= + // PINOCCHIO GENERATION METHODS + // ========================================================================= + + /// Generate token seeds structs (pinocchio version, uses BorshSerialize/BorshDeserialize). + fn generate_token_seeds_structs_pinocchio(&self) -> TokenStream { + let structs: Vec<_> = self + .token_seeds + .iter() + .map(|spec| { + let variant_name = &spec.variant; + let seeds_name = format_ident!("{}Seeds", variant_name); + let packed_seeds_name = format_ident!("Packed{}Seeds", variant_name); + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + let unpacked_fields: Vec<_> = ctx_fields + .iter() + .map(|f| quote! { pub #f: [u8; 32] }) + .collect(); + + let packed_fields: Vec<_> = ctx_fields + .iter() + .map(|f| { + let idx = format_ident!("{}_idx", f); + quote! { pub #idx: u8 } + }) + .collect(); + + let pack_stmts: Vec<_> = ctx_fields + .iter() + .map(|f| { + let idx = format_ident!("{}_idx", f); + quote! { #idx: remaining_accounts.insert_or_get(solana_pubkey::Pubkey::from(self.#f)) } + }) + .collect(); + + let bump_seed_refs: Vec<_> = spec + .seeds + .iter() + .map(seed_to_unpacked_ref) + .collect(); + + let unpack_resolve_stmts: Vec<_> = ctx_fields + .iter() + .map(|f| { + let idx = format_ident!("{}_idx", f); + quote! { + let #f: [u8; 32] = + remaining_accounts + .get(self.#idx as usize) + .ok_or(light_account_pinocchio::LightSdkTypesError::InvalidInstructionData)? + .key(); + } + }) + .collect(); + + let unpack_field_assigns: Vec<_> = ctx_fields.iter().map(|f| quote! { #f }).collect(); + + let seeds_struct = if unpacked_fields.is_empty() { + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub struct #seeds_name; + } + } else { + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub struct #seeds_name { + #(#unpacked_fields,)* + } + } + }; + + quote! { + #seeds_struct + + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub struct #packed_seeds_name { + #(#packed_fields,)* + pub bump: u8, + } + + #[cfg(not(target_os = "solana"))] + impl light_account_pinocchio::Pack for #seeds_name { + type Packed = #packed_seeds_name; + + fn pack( + &self, + remaining_accounts: &mut light_account_pinocchio::PackedAccounts, + ) -> std::result::Result { + let __seeds: &[&[u8]] = &[#(#bump_seed_refs),*]; + let (_, __bump) = solana_pubkey::Pubkey::find_program_address( + __seeds, + &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id), + ); + Ok(#packed_seeds_name { + #(#pack_stmts,)* + bump: __bump, + }) + } + } + + impl light_account_pinocchio::Unpack for #packed_seeds_name { + type Unpacked = #seeds_name; + + fn unpack( + &self, + remaining_accounts: &[AI], + ) -> std::result::Result { + #(#unpack_resolve_stmts)* + Ok(#seeds_name { + #(#unpack_field_assigns,)* + }) + } + } + } + }) + .collect(); + + quote! { #(#structs)* } + } + + /// Generate token variant trait impls (pinocchio version). + fn generate_token_variant_trait_impls_pinocchio(&self) -> TokenStream { + let impls: Vec<_> = self + .token_seeds + .iter() + .map(|spec| { + let seeds_name = format_ident!("{}Seeds", spec.variant); + let packed_seeds_name = format_ident!("Packed{}Seeds", spec.variant); + let seed_count = spec.seeds.len() + 1; + + let unpacked_seed_ref_items: Vec<_> = spec + .seeds + .iter() + .map(seed_to_unpacked_ref) + .collect(); + + let seed_vec_items: Vec<_> = spec + .seeds + .iter() + .map(|seed| { + match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + quote! { #value.as_bytes().to_vec() } + } + SeedElement::Expression(expr) => { + if let Some(field_name) = extract_ctx_field_from_expr(expr) { + quote! { self.#field_name.as_ref().to_vec() } + } else { + 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! { vec![#(#bytes),*] }; + } + } + if let syn::Expr::Path(path_expr) = &**expr { + if path_expr.qself.is_none() { + if let Some(last_seg) = path_expr.path.segments.last() { + if crate::light_pdas::shared_utils::is_constant_identifier(&last_seg.ident.to_string()) { + let path = &path_expr.path; + return quote! { { let __seed: &[u8] = #path.as_ref(); __seed.to_vec() } }; + } + } + } + } + quote! { { let __seed: &[u8] = (#expr).as_ref(); __seed.to_vec() } } + } + } + } + }) + .collect(); + + let pinocchio_crate = quote! { light_account_pinocchio }; + let packed_seed_ref_items: Vec<_> = spec + .seeds + .iter() + .map(|s| seed_to_packed_ref_with_crate(s, &pinocchio_crate)) + .collect(); + + let owner_derivation = if let Some(owner_seeds) = &spec.owner_seeds { + let owner_seed_refs: Vec<_> = owner_seeds + .iter() + .map(|seed| { + match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + quote! { #value.as_bytes() } + } + SeedElement::Expression(expr) => { + quote! { { let __seed: &[u8] = (#expr).as_ref(); __seed } } + } + } + }) + .collect(); + quote! { + let (__owner, _) = solana_pubkey::Pubkey::find_program_address( + &[#(#owner_seed_refs),*], + &solana_pubkey::Pubkey::from(crate::LIGHT_CPI_SIGNER.program_id), + ); + __owner.to_bytes() + } + } else { + quote! { [0u8; 32] } + }; + + quote! { + impl light_account_pinocchio::UnpackedTokenSeeds<#seed_count> + for #seeds_name + { + type Packed = #packed_seeds_name; + + const PROGRAM_ID: [u8; 32] = crate::LIGHT_CPI_SIGNER.program_id; + + fn seed_vec(&self) -> Vec> { + vec![#(#seed_vec_items),*] + } + + fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; #seed_count] { + [#(#unpacked_seed_ref_items,)* bump_storage] + } + } + + impl light_account_pinocchio::PackedTokenSeeds<#seed_count> + for #packed_seeds_name + { + type Unpacked = #seeds_name; + + fn bump(&self) -> u8 { + self.bump + } + + fn unpack_seeds( + &self, + accounts: &[AI], + ) -> std::result::Result { + >::unpack(self, accounts) + } + + fn seed_refs_with_bump<'a, AI: light_account_pinocchio::light_account_checks::AccountInfoTrait>( + &'a self, + accounts: &'a [AI], + bump_storage: &'a [u8; 1], + ) -> std::result::Result<[&'a [u8]; #seed_count], light_account_pinocchio::LightSdkTypesError> { + Ok([#(#packed_seed_ref_items,)* bump_storage]) + } + + fn derive_owner(&self) -> [u8; 32] { + #owner_derivation + } + } + } + }) + .collect(); + + quote! { #(#impls)* } + } + + /// Generate unpacked enum (pinocchio version). + fn generate_unpacked_enum_pinocchio(&self) -> TokenStream { + let pda_variants: Vec<_> = self + .pda_ctx_seeds + .iter() + .map(|info| { + let variant_name = &info.variant_name; + let seeds_type = format_ident!("{}Seeds", variant_name); + let inner_type = qualify_type_with_crate(&info.inner_type); + quote! { #variant_name { seeds: #seeds_type, data: #inner_type } } + }) + .collect(); + + let token_variants: Vec<_> = self + .token_seeds + .iter() + .map(|spec| { + let variant_name = &spec.variant; + let seeds_name = format_ident!("{}Seeds", variant_name); + quote! { + #variant_name(light_account_pinocchio::token::TokenDataWithSeeds<#seeds_name>) + } + }) + .collect(); + + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub enum LightAccountVariant { + #(#pda_variants,)* + #(#token_variants,)* + } + } + } + + /// Generate packed enum (pinocchio version). + fn generate_packed_enum_pinocchio(&self) -> TokenStream { + let pda_variants: Vec<_> = + self.pda_ctx_seeds + .iter() + .map(|info| { + let variant_name = &info.variant_name; + let packed_seeds_type = format_ident!("Packed{}Seeds", variant_name); + let inner_type = &info.inner_type; + let packed_data_type = + crate::light_pdas::shared_utils::make_packed_type(inner_type) + .unwrap_or_else(|| { + let type_str = quote!(#inner_type).to_string().replace(' ', ""); + let packed_name = format_ident!("Packed{}", type_str); + syn::parse_quote!(#packed_name) + }); + quote! { #variant_name { seeds: #packed_seeds_type, data: #packed_data_type } } + }) + .collect(); + + let token_variants: Vec<_> = self + .token_seeds + .iter() + .map(|spec| { + let variant_name = &spec.variant; + let packed_seeds_name = format_ident!("Packed{}Seeds", variant_name); + quote! { + #variant_name(light_account_pinocchio::token::TokenDataWithPackedSeeds<#packed_seeds_name>) + } + }) + .collect(); + + quote! { + #[derive(borsh::BorshSerialize, borsh::BorshDeserialize, Clone, Debug)] + pub enum PackedLightAccountVariant { + #(#pda_variants,)* + #(#token_variants,)* + } + } + } + + /// Generate LightAccountData struct (pinocchio version). + fn generate_light_account_data_struct_pinocchio(&self) -> TokenStream { + quote! { + #[derive(Clone, Debug, borsh::BorshSerialize, borsh::BorshDeserialize)] + pub struct LightAccountData { + pub meta: light_account_pinocchio::account_meta::CompressedAccountMetaNoLamportsNoAddress, + pub data: PackedLightAccountVariant, + } + } + } + + /// Generate DecompressVariant impl (pinocchio version). + fn generate_decompress_variant_impl_pinocchio(&self) -> TokenStream { + let pda_arms: Vec<_> = self + .pda_ctx_seeds + .iter() + .map(|info| { + let variant_name = &info.variant_name; + let packed_variant_type = format_ident!("Packed{}Variant", variant_name); + let seed_count = info.seed_count; + + quote! { + Self::#variant_name { seeds, data } => { + let packed_data = #packed_variant_type { seeds: seeds.clone(), data: data.clone() }; + light_account_pinocchio::prepare_account_for_decompression::<#seed_count, #packed_variant_type, pinocchio::account_info::AccountInfo>( + &packed_data, + tree_info, + output_queue_index, + pda_account, + ctx, + ) + } + } + }) + .collect(); + + let token_arms: Vec<_> = self + .token_seeds + .iter() + .map(|spec| { + let variant_name = &spec.variant; + let packed_seeds_name = format_ident!("Packed{}Seeds", variant_name); + let seed_count = spec.seeds.len() + 1; + + quote! { + Self::#variant_name(packed_data) => { + light_account_pinocchio::token::prepare_token_account_for_decompression::< + #seed_count, + light_account_pinocchio::token::TokenDataWithPackedSeeds<#packed_seeds_name>, + pinocchio::account_info::AccountInfo, + >( + packed_data, + tree_info, + output_queue_index, + pda_account, + ctx, + ) + } + } + }) + .collect(); + + quote! { + impl light_account_pinocchio::DecompressVariant for PackedLightAccountVariant { + fn decompress( + &self, + tree_info: &light_account_pinocchio::PackedStateTreeInfo, + pda_account: &pinocchio::account_info::AccountInfo, + ctx: &mut light_account_pinocchio::DecompressCtx<'_>, + ) -> std::result::Result<(), light_account_pinocchio::LightSdkTypesError> { + let output_queue_index = ctx.output_queue_index; + match self { + #(#pda_arms)* + #(#token_arms)* + } + } + } + } + } + + /// Generate Pack impl (pinocchio version). + fn generate_pack_impl_pinocchio(&self) -> TokenStream { + let pda_arms: Vec<_> = self + .pda_ctx_seeds + .iter() + .map(|info| { + let variant_name = &info.variant_name; + let variant_struct_name = format_ident!("{}Variant", variant_name); + + quote! { + Self::#variant_name { seeds, data } => { + let variant = #variant_struct_name { seeds: seeds.clone(), data: data.clone() }; + let packed = light_account_pinocchio::Pack::pack(&variant, accounts)?; + Ok(PackedLightAccountVariant::#variant_name { seeds: packed.seeds, data: packed.data }) + } + } + }) + .collect(); + + let token_arms: Vec<_> = self + .token_seeds + .iter() + .map(|spec| { + let variant_name = &spec.variant; + quote! { + Self::#variant_name(data) => { + let packed = light_account_pinocchio::Pack::pack(data, accounts)?; + Ok(PackedLightAccountVariant::#variant_name(packed)) + } + } + }) + .collect(); + + quote! { + #[cfg(not(target_os = "solana"))] + impl light_account_pinocchio::Pack for LightAccountVariant { + type Packed = PackedLightAccountVariant; + + fn pack( + &self, + accounts: &mut light_account_pinocchio::PackedAccounts, + ) -> std::result::Result { match self { #(#pda_arms)* #(#token_arms)* @@ -716,7 +1219,9 @@ fn seed_to_unpacked_ref(seed: &SeedElement) -> TokenStream { } /// Generate a seed ref expression for the PACKED variant (uses `accounts[idx].key.as_ref()`). -fn seed_to_packed_ref(seed: &SeedElement) -> TokenStream { +/// +/// `account_crate` selects the error path: `light_account` for Anchor, `light_account_pinocchio` for pinocchio. +fn seed_to_packed_ref_with_crate(seed: &SeedElement, account_crate: &TokenStream) -> TokenStream { match seed { SeedElement::Literal(lit) => { let value = lit.value(); @@ -746,12 +1251,17 @@ fn seed_to_packed_ref(seed: &SeedElement) -> TokenStream { return quote! { accounts .get(self.#idx_field as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? - .key - .as_ref() + .ok_or(#account_crate::LightSdkTypesError::InvalidInstructionData)? + .key_ref() }; } quote! { { let __seed: &[u8] = (#expr).as_ref(); __seed } } } } } + +/// Anchor-compatible wrapper. +fn seed_to_packed_ref(seed: &SeedElement) -> TokenStream { + let crate_path = quote! { light_account }; + seed_to_packed_ref_with_crate(seed, &crate_path) +} diff --git a/sdk-libs/program-test/Cargo.toml b/sdk-libs/program-test/Cargo.toml index 6f1f5524e6..36d357473b 100644 --- a/sdk-libs/program-test/Cargo.toml +++ b/sdk-libs/program-test/Cargo.toml @@ -12,6 +12,7 @@ devenv = ["v2", "light-client/devenv", "light-prover-client/devenv", "dep:accoun [dependencies] light-sdk = { workspace = true, features = ["anchor"] } +light-account = { workspace = true } light-account-checks = { workspace = true } light-indexed-merkle-tree = { workspace = true, features = ["solana"] } light-indexed-array = { workspace = true } diff --git a/sdk-libs/program-test/src/compressible.rs b/sdk-libs/program-test/src/compressible.rs index adcc868a76..00138b5d7b 100644 --- a/sdk-libs/program-test/src/compressible.rs +++ b/sdk-libs/program-test/src/compressible.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; use borsh::BorshDeserialize; +use light_account::LightConfig; use light_account_checks::discriminator::DISCRIMINATOR_LEN; use light_client::rpc::{Rpc, RpcError}; use light_compressible::{ @@ -8,7 +9,6 @@ use light_compressible::{ config::CompressibleConfig as CtokenCompressibleConfig, rent::{RentConfig, SLOTS_PER_EPOCH}, }; -use light_sdk::interface::LightConfig; use light_token_interface::{ state::{Mint, Token, ACCOUNT_TYPE_MINT, ACCOUNT_TYPE_TOKEN_ACCOUNT}, LIGHT_TOKEN_PROGRAM_ID, @@ -271,7 +271,10 @@ pub async fn auto_compress_program_pdas( let payer = rpc.get_payer().insecure_clone(); - let config_pda = LightConfig::derive_pda(&program_id, 0).0; + let (config_pda, _) = Pubkey::find_program_address( + &[light_account::LIGHT_CONFIG_SEED, &0u16.to_le_bytes()], + &program_id, + ); let cfg_acc_opt = rpc.get_account(config_pda).await?; let Some(cfg_acc) = cfg_acc_opt else { @@ -279,10 +282,10 @@ pub async fn auto_compress_program_pdas( }; let cfg = LightConfig::try_from_slice(&cfg_acc.data[DISCRIMINATOR_LEN..]) .map_err(|e| RpcError::CustomError(format!("config deserialize: {e:?}")))?; - let rent_sponsor = cfg.rent_sponsor; + let rent_sponsor = Pubkey::from(cfg.rent_sponsor); // compression_authority is the payer by default for auto-compress let compression_authority = payer.pubkey(); - let address_tree = cfg.address_space[0]; + let address_tree = Pubkey::from(cfg.address_space[0]); let program_accounts = rpc.context.get_program_accounts(&program_id); diff --git a/sdk-libs/program-test/src/forester/compress_and_close_forester.rs b/sdk-libs/program-test/src/forester/compress_and_close_forester.rs index 1ac543bc3d..15ab7c5889 100644 --- a/sdk-libs/program-test/src/forester/compress_and_close_forester.rs +++ b/sdk-libs/program-test/src/forester/compress_and_close_forester.rs @@ -1,10 +1,10 @@ +use light_account::PackedAccounts; use light_client::{ indexer::Indexer, rpc::{Rpc, RpcError}, }; use light_compressed_token_sdk::compressed_token::CompressAndCloseAccounts as CTokenCompressAndCloseAccounts; use light_compressible::config::CompressibleConfig; -use light_sdk::instruction::PackedAccounts; use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signature}, 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 4ddf60d021..1149e8804a 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 @@ -166,7 +166,7 @@ impl LightProgramTest { context.auto_mine_cold_state_programs.push(pid); } // Airdrop to program's rent sponsor PDA for decompression - let (rent_sponsor, _) = light_sdk::utils::derive_rent_sponsor_pda(&pid); + let (rent_sponsor, _) = light_account::derive_rent_sponsor_pda(&pid); context .context .airdrop(&rent_sponsor, 100_000_000_000) diff --git a/sdk-libs/sdk-pinocchio/src/error.rs b/sdk-libs/sdk-pinocchio/src/error.rs index 921b079374..0699705e9a 100644 --- a/sdk-libs/sdk-pinocchio/src/error.rs +++ b/sdk-libs/sdk-pinocchio/src/error.rs @@ -84,6 +84,26 @@ pub enum LightSdkError { ProgramError(ProgramError), #[error(transparent)] AccountError(#[from] AccountError), + #[error("Missing compression info")] + MissingCompressionInfo, + #[error("Invalid rent sponsor")] + InvalidRentSponsor, + #[error("Borsh IO error: {0}")] + BorshIo(String), + #[error("Read-only accounts not supported in CPI context")] + ReadOnlyAccountsNotSupportedInCpiContext, + #[error("Account data too small")] + AccountDataTooSmall, + #[error("Invalid instruction data")] + InvalidInstructionData, + #[error("Invalid seeds")] + InvalidSeeds, + #[error("CPI failed")] + CpiFailed, + #[error("Not enough account keys")] + NotEnoughAccountKeys, + #[error("Missing required signature")] + MissingRequiredSignature, } impl From for LightSdkError { @@ -131,6 +151,24 @@ impl From for LightSdkError { LightSdkTypesError::InvalidSolPoolPdaAccount => LightSdkError::InvalidSolPoolPdaAccount, LightSdkTypesError::AccountError(e) => LightSdkError::AccountError(e), LightSdkTypesError::InvalidCpiAccountsOffset => LightSdkError::InvalidCpiAccountsOffset, + LightSdkTypesError::ConstraintViolation => LightSdkError::ConstraintViolation, + LightSdkTypesError::Borsh => LightSdkError::Borsh, + LightSdkTypesError::MissingCompressionInfo => LightSdkError::MissingCompressionInfo, + LightSdkTypesError::InvalidRentSponsor => LightSdkError::InvalidRentSponsor, + LightSdkTypesError::BorshIo(s) => LightSdkError::BorshIo(s), + LightSdkTypesError::ReadOnlyAccountsNotSupportedInCpiContext => { + LightSdkError::ReadOnlyAccountsNotSupportedInCpiContext + } + LightSdkTypesError::CompressedAccountError(e) => LightSdkError::CompressedAccount(e), + LightSdkTypesError::AccountDataTooSmall => LightSdkError::AccountDataTooSmall, + LightSdkTypesError::InvalidInstructionData => LightSdkError::InvalidInstructionData, + LightSdkTypesError::InvalidSeeds => LightSdkError::InvalidSeeds, + LightSdkTypesError::CpiFailed => LightSdkError::CpiFailed, + LightSdkTypesError::NotEnoughAccountKeys => LightSdkError::NotEnoughAccountKeys, + LightSdkTypesError::MissingRequiredSignature => LightSdkError::MissingRequiredSignature, + LightSdkTypesError::ProgramError(code) => { + LightSdkError::ProgramError(ProgramError::Custom(code)) + } } } } @@ -176,6 +214,16 @@ impl From for u32 { LightSdkError::CompressedAccount(_) => 16036, LightSdkError::ProgramError(e) => u64::from(e) as u32, LightSdkError::AccountError(e) => e.into(), + LightSdkError::MissingCompressionInfo => 16037, + LightSdkError::InvalidRentSponsor => 16038, + LightSdkError::BorshIo(_) => 16039, + LightSdkError::ReadOnlyAccountsNotSupportedInCpiContext => 16040, + LightSdkError::AccountDataTooSmall => 16041, + LightSdkError::InvalidInstructionData => 16042, + LightSdkError::InvalidSeeds => 16043, + LightSdkError::CpiFailed => 16044, + LightSdkError::NotEnoughAccountKeys => 16045, + LightSdkError::MissingRequiredSignature => 16046, } } } diff --git a/sdk-libs/sdk-types/Cargo.toml b/sdk-libs/sdk-types/Cargo.toml index 7bad4c1b9b..25bd67ac6e 100644 --- a/sdk-libs/sdk-types/Cargo.toml +++ b/sdk-libs/sdk-types/Cargo.toml @@ -11,8 +11,10 @@ default = ["std", "v2"] std = ["alloc", "light-compressed-account/std", "solana-msg"] alloc = ["light-compressed-account/alloc"] keccak = ["light-hasher/keccak"] -anchor = ["anchor-lang", "light-compressed-account/anchor"] -idl-build = ["anchor-lang/idl-build", "anchor"] +sha256 = ["light-hasher/sha256", "light-compressed-account/sha256"] +token = ["dep:light-token-interface"] +anchor = ["anchor-lang", "light-compressed-account/anchor", "light-compressible/anchor", "solana-program-error"] +idl-build = ["anchor-lang/idl-build", "light-compressible/idl-build", "anchor"] v2 = [] cpi-context = [] poseidon = ["light-hasher/poseidon", "light-compressed-account/poseidon"] @@ -23,12 +25,23 @@ anchor-lang = { workspace = true, optional = true } light-account-checks = { workspace = true } light-hasher = { workspace = true } light-compressed-account = { workspace = true } +light-compressible = { workspace = true } light-macros = { workspace = true } +light-token-interface = { workspace = true, optional = true } solana-msg = { workspace = true, optional = true } +solana-program-error = { workspace = true, optional = true } # External dependencies borsh = { workspace = true } +bytemuck = { workspace = true } thiserror = { workspace = true } [dev-dependencies] solana-pubkey = { workspace = true } + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] diff --git a/sdk-libs/sdk-types/src/constants.rs b/sdk-libs/sdk-types/src/constants.rs index 2a6f18f7cb..68d5c59c1d 100644 --- a/sdk-libs/sdk-types/src/constants.rs +++ b/sdk-libs/sdk-types/src/constants.rs @@ -33,3 +33,11 @@ pub const CPI_CONTEXT_ACCOUNT_2_DISCRIMINATOR: [u8; 8] = [34, 184, 183, 14, 100, pub const SOL_POOL_PDA: [u8; 32] = pubkey_array!("CHK57ywWSDncAoRu1F8QgwYJeXuAJyyBYT4LixLXvMZ1"); pub const ADDRESS_TREE_V2: [u8; 32] = pubkey_array!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"); + +/// Default compressible config PDA for the Light Token Program (V1). +pub const LIGHT_TOKEN_CONFIG: [u8; 32] = + pubkey_array!("ACXg8a7VaqecBWrSbdu73W4Pg9gsqXJ3EXAqkHyhvVXg"); + +/// Default rent sponsor PDA for the Light Token Program (V1). +pub const LIGHT_TOKEN_RENT_SPONSOR: [u8; 32] = + pubkey_array!("r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti"); diff --git a/sdk-libs/sdk-types/src/cpi_accounts/v2.rs b/sdk-libs/sdk-types/src/cpi_accounts/v2.rs index 9afb32facf..98de6e19cd 100644 --- a/sdk-libs/sdk-types/src/cpi_accounts/v2.rs +++ b/sdk-libs/sdk-types/src/cpi_accounts/v2.rs @@ -208,6 +208,7 @@ impl<'a, T: AccountInfoTrait + Clone> CpiAccounts<'a, T> { } /// Create a vector of account info references + #[cfg(any(feature = "std", feature = "alloc"))] pub fn to_account_infos(&self) -> Vec { let mut account_infos = Vec::with_capacity(1 + self.accounts.len()); account_infos.push(self.fee_payer().clone()); @@ -240,6 +241,7 @@ impl<'a, T: AccountInfoTrait + Clone> CpiAccounts<'a, T> { Ok(&self.accounts[system_offset..]) } + #[cfg(any(feature = "std", feature = "alloc"))] pub fn tree_pubkeys(&self) -> Result> { Ok(self .tree_accounts()? diff --git a/sdk-libs/sdk-types/src/error.rs b/sdk-libs/sdk-types/src/error.rs index d766449ce8..23ded4116d 100644 --- a/sdk-libs/sdk-types/src/error.rs +++ b/sdk-libs/sdk-types/src/error.rs @@ -1,4 +1,8 @@ +#[cfg(all(not(feature = "std"), feature = "alloc"))] +use alloc::string::String; + use light_account_checks::error::AccountError; +use light_compressed_account::CompressedAccountError; use light_hasher::HasherError; use thiserror::Error; @@ -32,12 +36,72 @@ pub enum LightSdkTypesError { InvalidCpiContextAccount, #[error("Invalid sol pool pda account")] InvalidSolPoolPdaAccount, - #[error("CpigAccounts accounts slice starts with an invalid account. It should start with LightSystemProgram SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7.")] + #[error("CpiAccounts accounts slice starts with an invalid account. It should start with LightSystemProgram SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7.")] InvalidCpiAccountsOffset, #[error(transparent)] AccountError(#[from] AccountError), #[error(transparent)] Hasher(#[from] HasherError), + // --- Variants merged from LightSdkTypesError (sdk-interface) --- + #[error("Constraint violation")] + ConstraintViolation, + #[error("Borsh serialization/deserialization error")] + Borsh, + #[error("Missing compression info")] + MissingCompressionInfo, + #[error("Invalid rent sponsor")] + InvalidRentSponsor, + #[cfg(feature = "alloc")] + #[error("Borsh IO error: {0}")] + BorshIo(String), + #[error("Read-only accounts not supported in CPI context")] + ReadOnlyAccountsNotSupportedInCpiContext, + #[error(transparent)] + CompressedAccountError(#[from] CompressedAccountError), + #[error("Account data too small")] + AccountDataTooSmall, + #[error("Invalid instruction data")] + InvalidInstructionData, + #[error("Invalid seeds")] + InvalidSeeds, + #[error("CPI failed")] + CpiFailed, + #[error("Not enough account keys")] + NotEnoughAccountKeys, + #[error("Missing required signature")] + MissingRequiredSignature, + #[error("Program error: {0}")] + ProgramError(u32), +} + +#[cfg(feature = "anchor")] +impl From for anchor_lang::error::Error { + fn from(e: LightSdkTypesError) -> Self { + anchor_lang::error::Error::from(solana_program_error::ProgramError::Custom(u32::from(e))) + } +} + +#[cfg(feature = "anchor")] +impl From for solana_program_error::ProgramError { + fn from(e: LightSdkTypesError) -> Self { + solana_program_error::ProgramError::Custom(u32::from(e)) + } +} + +#[cfg(feature = "anchor")] +impl From for LightSdkTypesError { + fn from(e: solana_program_error::ProgramError) -> Self { + match e { + solana_program_error::ProgramError::InvalidAccountData => { + LightSdkTypesError::InvalidInstructionData + } + solana_program_error::ProgramError::BorshIoError(_) => LightSdkTypesError::Borsh, + solana_program_error::ProgramError::AccountBorrowFailed => { + LightSdkTypesError::ConstraintViolation + } + other => LightSdkTypesError::ProgramError(u64::from(other) as u32), + } + } } impl From for u32 { @@ -59,6 +123,21 @@ impl From for u32 { LightSdkTypesError::InvalidCpiAccountsOffset => 14034, LightSdkTypesError::AccountError(e) => e.into(), LightSdkTypesError::Hasher(e) => e.into(), + LightSdkTypesError::ConstraintViolation => 14035, + LightSdkTypesError::Borsh => 14036, + LightSdkTypesError::MissingCompressionInfo => 14037, + LightSdkTypesError::InvalidRentSponsor => 14038, + #[cfg(feature = "alloc")] + LightSdkTypesError::BorshIo(_) => 14039, + LightSdkTypesError::ReadOnlyAccountsNotSupportedInCpiContext => 14040, + LightSdkTypesError::CompressedAccountError(_) => 14041, + LightSdkTypesError::AccountDataTooSmall => 14042, + LightSdkTypesError::InvalidInstructionData => 14043, + LightSdkTypesError::InvalidSeeds => 14044, + LightSdkTypesError::CpiFailed => 14045, + LightSdkTypesError::NotEnoughAccountKeys => 14046, + LightSdkTypesError::MissingRequiredSignature => 14047, + LightSdkTypesError::ProgramError(code) => code, } } } diff --git a/sdk-libs/sdk/src/interface/account/compression_info.rs b/sdk-libs/sdk-types/src/interface/account/compression_info.rs similarity index 69% rename from sdk-libs/sdk/src/interface/account/compression_info.rs rename to sdk-libs/sdk-types/src/interface/account/compression_info.rs index c32aa6022d..caf30bc6a9 100644 --- a/sdk-libs/sdk/src/interface/account/compression_info.rs +++ b/sdk-libs/sdk-types/src/interface/account/compression_info.rs @@ -1,30 +1,19 @@ -use std::borrow::Cow; +extern crate alloc; +use alloc::{borrow::Cow, string::ToString}; use bytemuck::{Pod, Zeroable}; +use light_account_checks::AccountInfoTrait; use light_compressible::rent::RentConfig; -use light_sdk_types::instruction::PackedStateTreeInfo; -use solana_account_info::AccountInfo; -use solana_clock::Clock; -use solana_cpi::invoke; -use solana_instruction::{AccountMeta, Instruction}; -use solana_pubkey::Pubkey; -use solana_sysvar::Sysvar; - -use super::pack::Unpack; -use crate::{AnchorDeserialize, AnchorSerialize, ProgramError}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] -#[repr(u8)] -pub enum AccountState { - Initialized, - Frozen, -} + +use crate::{ + error::LightSdkTypesError, instruction::PackedStateTreeInfo, AnchorDeserialize, AnchorSerialize, +}; pub trait HasCompressionInfo { - fn compression_info(&self) -> Result<&CompressionInfo, ProgramError>; - fn compression_info_mut(&mut self) -> Result<&mut CompressionInfo, ProgramError>; + fn compression_info(&self) -> Result<&CompressionInfo, LightSdkTypesError>; + fn compression_info_mut(&mut self) -> Result<&mut CompressionInfo, LightSdkTypesError>; fn compression_info_mut_opt(&mut self) -> &mut Option; - fn set_compression_info_none(&mut self) -> Result<(), ProgramError>; + fn set_compression_info_none(&mut self) -> Result<(), LightSdkTypesError>; } /// Simple field accessor trait for types with a `compression_info: Option` field. @@ -52,7 +41,7 @@ pub trait CompressionInfoField { fn write_decompressed_info_to_slice( data: &mut [u8], current_slot: u64, - ) -> Result<(), ProgramError> { + ) -> Result<(), LightSdkTypesError> { use crate::AnchorSerialize; let info = CompressionInfo { @@ -74,7 +63,7 @@ pub trait CompressionInfoField { }; if data.len() < offset + option_size { - return Err(ProgramError::AccountDataTooSmall); + return Err(LightSdkTypesError::AccountDataTooSmall); } let target = &mut data[offset..offset + option_size]; @@ -82,30 +71,30 @@ pub trait CompressionInfoField { target[0] = 1; // Write CompressionInfo info.serialize(&mut &mut target[1..]) - .map_err(|_| ProgramError::BorshIoError("compression_info serialize failed".into()))?; + .map_err(|e| LightSdkTypesError::BorshIo(e.to_string()))?; Ok(()) } } impl HasCompressionInfo for T { - fn compression_info(&self) -> Result<&CompressionInfo, ProgramError> { + fn compression_info(&self) -> Result<&CompressionInfo, LightSdkTypesError> { self.compression_info_field() .as_ref() - .ok_or(crate::error::LightSdkError::MissingCompressionInfo.into()) + .ok_or(LightSdkTypesError::MissingCompressionInfo) } - fn compression_info_mut(&mut self) -> Result<&mut CompressionInfo, ProgramError> { + fn compression_info_mut(&mut self) -> Result<&mut CompressionInfo, LightSdkTypesError> { self.compression_info_field_mut() .as_mut() - .ok_or(crate::error::LightSdkError::MissingCompressionInfo.into()) + .ok_or(LightSdkTypesError::MissingCompressionInfo) } fn compression_info_mut_opt(&mut self) -> &mut Option { self.compression_info_field_mut() } - fn set_compression_info_none(&mut self) -> Result<(), ProgramError> { + fn set_compression_info_none(&mut self) -> Result<(), LightSdkTypesError> { *self.compression_info_field_mut() = None; Ok(()) } @@ -121,7 +110,6 @@ pub trait CompressAs { type Output: crate::AnchorSerialize + crate::AnchorDeserialize + crate::LightDiscriminator - + crate::account::Size + HasCompressionInfo + Default + Clone; @@ -203,7 +191,10 @@ impl CompressionInfo { /// Rent sponsor is always the config's rent_sponsor (not stored per-account). /// This means rent always flows to the protocol's rent pool upon compression, /// regardless of who paid for account creation. - pub fn new_from_config(cfg: &crate::interface::LightConfig, current_slot: u64) -> Self { + pub fn new_from_config( + cfg: &crate::interface::program::config::LightConfig, + current_slot: u64, + ) -> Self { Self { last_claimed_slot: current_slot, lamports_per_write: cfg.write_top_up, @@ -214,23 +205,22 @@ impl CompressionInfo { } } - /// Backward-compat constructor used by older call sites; initializes minimal fields. + /// Backward-compat constructor; initializes minimal fields. /// Rent will flow to config's rent_sponsor upon compression. - pub fn new_decompressed() -> Result { - Ok(Self { - last_claimed_slot: Clock::get()?.slot, + pub fn new_decompressed(current_slot: u64) -> Self { + Self { + last_claimed_slot: current_slot, lamports_per_write: 0, config_version: 0, state: CompressionState::Decompressed, _padding: 0, rent_config: RentConfig::default(), - }) + } } - /// Update last_claimed_slot to the current slot. - pub fn bump_last_claimed_slot(&mut self) -> Result<(), crate::ProgramError> { - self.last_claimed_slot = Clock::get()?.slot; - Ok(()) + /// Update last_claimed_slot to the given slot. + pub fn bump_last_claimed_slot(&mut self, current_slot: u64) { + self.last_claimed_slot = current_slot; } /// Explicitly set last_claimed_slot. @@ -296,28 +286,21 @@ impl CompressionInfo { /// Top up rent on write if needed and transfer lamports from payer to account. /// This is the standard pattern for all write operations on compressible PDAs. + /// Generic over AccountInfoTrait to work with both solana and pinocchio. /// /// # Arguments /// * `account_info` - The PDA account to top up /// * `payer_info` - The payer account (will be debited) - /// * `system_program_info` - The System Program account for CPI - /// - /// # Returns - /// * `Ok(())` if top-up succeeded or was not needed - /// * `Err(ProgramError)` if transfer failed - pub fn top_up_rent<'a>( + pub fn top_up_rent( &self, - account_info: &AccountInfo<'a>, - payer_info: &AccountInfo<'a>, - system_program_info: &AccountInfo<'a>, - ) -> Result<(), crate::ProgramError> { - use solana_clock::Clock; - use solana_sysvar::{rent::Rent, Sysvar}; - + account_info: &AI, + payer_info: &AI, + ) -> Result<(), LightSdkTypesError> { let bytes = account_info.data_len() as u64; let current_lamports = account_info.lamports(); - let current_slot = Clock::get()?.slot; - let rent_exemption_lamports = Rent::get()?.minimum_balance(bytes as usize); + let current_slot = AI::get_current_slot().map_err(LightSdkTypesError::AccountError)?; + let rent_exemption_lamports = + AI::get_min_rent_balance(bytes as usize).map_err(LightSdkTypesError::AccountError)?; let top_up = self.calculate_top_up_lamports( bytes, @@ -327,10 +310,10 @@ impl CompressionInfo { ); if top_up > 0 { - // Use System Program CPI to transfer lamports - // This is required because the payer account is owned by the System Program, - // not by the calling program - transfer_lamports_cpi(payer_info, account_info, system_program_info, top_up)?; + // Use System Program CPI to transfer lamports (payer is a signer, pass empty seeds) + payer_info + .transfer_lamports_cpi(account_info, top_up, &[]) + .map_err(LightSdkTypesError::AccountError)?; } Ok(()) @@ -348,7 +331,7 @@ impl Space for CompressionInfo { #[cfg(feature = "anchor")] impl anchor_lang::Space for CompressionInfo { - const INIT_SPACE: usize = ::INIT_SPACE; + const INIT_SPACE: usize = core::mem::size_of::(); } /// Space required for Option when Some (1 byte discriminator + INIT_SPACE). @@ -366,33 +349,25 @@ pub struct CompressedAccountData { pub data: T, } -impl Unpack for CompressedAccountData> { - type Unpacked = Vec; - - fn unpack(&self, _remaining_accounts: &[AccountInfo]) -> Result { - unimplemented!() - } -} - /// Claim completed-epoch rent to the provided rent sponsor and update last_claimed_slot. /// Returns Some(claimed) if any lamports were claimed; None if account is compressible or nothing to claim. -pub fn claim_completed_epoch_rent<'info, A>( - account_info: &AccountInfo<'info>, +/// Generic over AccountInfoTrait to work with both solana and pinocchio. +pub fn claim_completed_epoch_rent( + account_info: &AI, account_data: &mut A, - rent_sponsor: &AccountInfo<'info>, -) -> Result, ProgramError> + rent_sponsor: &AI, +) -> Result, LightSdkTypesError> where + AI: AccountInfoTrait, A: HasCompressionInfo, { use light_compressible::rent::{AccountRentState, SLOTS_PER_EPOCH}; - use solana_sysvar::rent::Rent; - let current_slot = Clock::get()?.slot; + let current_slot = AI::get_current_slot().map_err(LightSdkTypesError::AccountError)?; let bytes = account_info.data_len() as u64; let current_lamports = account_info.lamports(); - let rent_exemption_lamports = Rent::get() - .map_err(|_| ProgramError::Custom(0))? - .minimum_balance(bytes as usize); + let rent_exemption_lamports = + AI::get_min_rent_balance(bytes as usize).map_err(LightSdkTypesError::AccountError)?; let ci = account_data.compression_info_mut()?; let state = AccountRentState { @@ -421,56 +396,13 @@ where .saturating_add(completed_epochs * SLOTS_PER_EPOCH), ); - // Transfer lamports to rent sponsor - { - let mut src = account_info - .try_borrow_mut_lamports() - .map_err(|_| ProgramError::Custom(0))?; - let mut dst = rent_sponsor - .try_borrow_mut_lamports() - .map_err(|_| ProgramError::Custom(0))?; - let new_src = src - .checked_sub(amount) - .ok_or(ProgramError::InsufficientFunds)?; - let new_dst = dst.checked_add(amount).ok_or(ProgramError::Custom(0))?; - **src = new_src; - **dst = new_dst; - } + // Transfer lamports to rent sponsor (direct lamport manipulation, no CPI needed + // since the program owns the account) + account_info + .transfer_lamports(rent_sponsor, amount) + .map_err(LightSdkTypesError::AccountError)?; return Ok(Some(amount)); } } Ok(Some(0)) } - -/// Transfer lamports from one account to another using System Program CPI. -/// This is required when transferring from accounts owned by the System Program. -/// -/// # Arguments -/// * `from` - Source account (owned by System Program) -/// * `to` - Destination account -/// * `system_program` - System Program account -/// * `lamports` - Amount of lamports to transfer -fn transfer_lamports_cpi<'a>( - from: &AccountInfo<'a>, - to: &AccountInfo<'a>, - system_program: &AccountInfo<'a>, - lamports: u64, -) -> Result<(), ProgramError> { - // System Program Transfer instruction discriminator: 2 (u32 little-endian) - let mut instruction_data = vec![2, 0, 0, 0]; - instruction_data.extend_from_slice(&lamports.to_le_bytes()); - - let transfer_instruction = Instruction { - program_id: Pubkey::default(), // System Program ID - accounts: vec![ - AccountMeta::new(*from.key, true), - AccountMeta::new(*to.key, false), - ], - data: instruction_data, - }; - - invoke( - &transfer_instruction, - &[from.clone(), to.clone(), system_program.clone()], - ) -} diff --git a/sdk-libs/sdk/src/interface/account/light_account.rs b/sdk-libs/sdk-types/src/interface/account/light_account.rs similarity index 56% rename from sdk-libs/sdk/src/interface/account/light_account.rs rename to sdk-libs/sdk-types/src/interface/account/light_account.rs index de61d89dbd..5f32da2471 100644 --- a/sdk-libs/sdk/src/interface/account/light_account.rs +++ b/sdk-libs/sdk-types/src/interface/account/light_account.rs @@ -1,17 +1,14 @@ //! LightAccount trait definition for compressible account data structs. -//! -//! This trait does NOT yet exist in the SDK - it is defined locally for this test -//! to demonstrate manual implementation without macros. -use anchor_lang::prelude::*; +#[cfg(all(not(target_os = "solana"), feature = "std"))] +use light_account_checks::AccountMetaTrait; +use light_account_checks::{packed_accounts::ProgramPackedAccounts, AccountInfoTrait}; use light_hasher::DataHasher; -use solana_program_error::ProgramError; use crate::{ - compressible::CompressionInfo, - instruction::PackedAccounts, - interface::LightConfig, - light_account_checks::{packed_accounts::ProgramPackedAccounts, AccountInfoTrait}, + error::LightSdkTypesError, + interface::{account::compression_info::CompressionInfo, program::config::LightConfig}, + AnchorDeserialize, AnchorSerialize, }; pub enum AccountType { @@ -51,15 +48,18 @@ pub trait LightAccount: /// Set compression info to decompressed state (used at decompression) fn set_decompressed(&mut self, config: &LightConfig, current_slot: u64); - /// Convert to packed form (Pubkeys -> indices) - fn pack( + /// Convert to packed form (Pubkeys -> indices). + /// Generic over AccountMetaTrait for runtime-agnostic packing. + #[cfg(all(not(target_os = "solana"), feature = "std"))] + fn pack( &self, - accounts: &mut PackedAccounts, - ) -> std::result::Result; + accounts: &mut crate::pack_accounts::PackedAccounts, + ) -> Result; - /// Convert from packed form (indices -> Pubkeys) - fn unpack( + /// Convert from packed form (indices -> Pubkeys). + /// Generic over AccountInfoTrait for runtime-agnostic unpacking. + fn unpack( packed: &Self::Packed, - accounts: &ProgramPackedAccounts, - ) -> std::result::Result; + accounts: &ProgramPackedAccounts, + ) -> Result; } diff --git a/sdk-libs/sdk/src/interface/account/mod.rs b/sdk-libs/sdk-types/src/interface/account/mod.rs similarity index 84% rename from sdk-libs/sdk/src/interface/account/mod.rs rename to sdk-libs/sdk-types/src/interface/account/mod.rs index 0d37bc0fe0..bf07f3e31f 100644 --- a/sdk-libs/sdk/src/interface/account/mod.rs +++ b/sdk-libs/sdk-types/src/interface/account/mod.rs @@ -4,11 +4,9 @@ //! including compression info, decompression, and closing. pub mod compression_info; +pub mod light_account; pub mod pack; pub mod pda_seeds; - -#[cfg(feature = "anchor")] -pub mod light_account; - -#[cfg(feature = "anchor")] +pub mod size; +#[cfg(feature = "token")] pub mod token_seeds; diff --git a/sdk-libs/sdk-types/src/interface/account/pack.rs b/sdk-libs/sdk-types/src/interface/account/pack.rs new file mode 100644 index 0000000000..c7a656ace4 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/account/pack.rs @@ -0,0 +1,25 @@ +//! Pack and Unpack traits for converting between full Pubkeys and u8 indices. + +use light_account_checks::AccountInfoTrait; +#[cfg(all(not(target_os = "solana"), feature = "std"))] +use light_account_checks::AccountMetaTrait; + +use crate::error::LightSdkTypesError; + +/// Replace 32-byte Pubkeys with 1-byte indices to save space. +/// If your type has no Pubkeys, just return self. +#[cfg(all(not(target_os = "solana"), feature = "std"))] +pub trait Pack { + type Packed: crate::AnchorSerialize + Clone + core::fmt::Debug; + + fn pack( + &self, + remaining_accounts: &mut crate::pack_accounts::PackedAccounts, + ) -> Result; +} + +pub trait Unpack { + type Unpacked; + + fn unpack(&self, remaining_accounts: &[AI]) -> Result; +} diff --git a/sdk-libs/sdk/src/interface/account/pda_seeds.rs b/sdk-libs/sdk-types/src/interface/account/pda_seeds.rs similarity index 56% rename from sdk-libs/sdk/src/interface/account/pda_seeds.rs rename to sdk-libs/sdk-types/src/interface/account/pda_seeds.rs index d553071e77..eac2cb8ff4 100644 --- a/sdk-libs/sdk/src/interface/account/pda_seeds.rs +++ b/sdk-libs/sdk-types/src/interface/account/pda_seeds.rs @@ -1,24 +1,21 @@ -// --- cpi-context-gated traits (from decompress_runtime.rs) --- +//! PDA seed derivation traits. -#[cfg(feature = "cpi-context")] -use solana_program_error::ProgramError; -#[cfg(feature = "cpi-context")] -use solana_pubkey::Pubkey; +use alloc::vec::Vec; + +use crate::error::LightSdkTypesError; /// Trait for account variants that can be checked for token or PDA type. -#[cfg(feature = "cpi-context")] pub trait HasTokenVariant { /// Returns true if this variant represents a token account (PackedTokenData). fn is_packed_token(&self) -> bool; } /// Trait for PDA types that can derive seeds with full account context access. -#[cfg(feature = "cpi-context")] pub trait PdaSeedDerivation { fn derive_pda_seeds_with_accounts( &self, - program_id: &Pubkey, + program_id: &[u8; 32], accounts: &A, seed_params: &S, - ) -> Result<(Vec>, Pubkey), ProgramError>; + ) -> Result<(Vec>, [u8; 32]), LightSdkTypesError>; } diff --git a/sdk-libs/sdk-types/src/interface/account/size.rs b/sdk-libs/sdk-types/src/interface/account/size.rs new file mode 100644 index 0000000000..9cf8be9e40 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/account/size.rs @@ -0,0 +1,8 @@ +//! Size trait for compressed accounts. + +use crate::error::LightSdkTypesError; + +/// Trait to get the serialized size of a compressed account. +pub trait Size { + fn size(&self) -> Result; +} diff --git a/sdk-libs/sdk-types/src/interface/account/token_seeds.rs b/sdk-libs/sdk-types/src/interface/account/token_seeds.rs new file mode 100644 index 0000000000..f22657590a --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/account/token_seeds.rs @@ -0,0 +1,297 @@ +//! Token seed types for packed/unpacked token account variants. +//! +//! Provides `TokenDataWithSeeds`, `PackedTokenData`, and `TokenDataWithPackedSeeds` +//! along with Pack/Unpack impls and blanket impls for variant traits. + +use alloc::{vec, vec::Vec}; + +use light_account_checks::AccountInfoTrait; +#[cfg(all(not(target_os = "solana"), feature = "std"))] +use light_account_checks::AccountMetaTrait; +use light_compressed_account::compressed_account::PackedMerkleContext; +pub use light_token_interface::{ + instructions::{ + extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, + transfer2::MultiInputTokenDataWithContext, + }, + state::{ + extensions::{CompressedOnlyExtension, ExtensionStruct}, + AccountState, Token, TokenDataVersion, + }, +}; + +use super::pack::Unpack; +#[cfg(all(not(target_os = "solana"), feature = "std"))] +use crate::interface::account::pack::Pack; +#[cfg(all(not(target_os = "solana"), feature = "std"))] +use crate::pack_accounts::PackedAccounts; +use crate::{ + error::LightSdkTypesError, + instruction::PackedStateTreeInfo, + interface::{ + account::light_account::AccountType, + program::variant::{ + LightAccountVariantTrait, PackedLightAccountVariantTrait, PackedTokenSeeds, + UnpackedTokenSeeds, + }, + }, + AnchorDeserialize, AnchorSerialize, +}; + +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub struct TokenDataWithSeeds { + pub seeds: S, + pub token_data: Token, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, Default, PartialEq, AnchorSerialize, AnchorDeserialize)] +pub struct PackedTokenData { + pub owner: u8, + pub amount: u64, + pub has_delegate: bool, + pub delegate: u8, + pub mint: u8, + pub version: u8, +} + +#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] +pub struct TokenDataWithPackedSeeds< + S: AnchorSerialize + AnchorDeserialize + Clone + core::fmt::Debug, +> { + pub seeds: S, + pub token_data: PackedTokenData, + pub extension: Option, +} + +// ============================================================================= +// Helper: unpack token data from packed indices +// ============================================================================= + +fn unpack_token_data_from_packed( + packed: &PackedTokenData, + extension: &Option, + accounts: &[AI], +) -> Result { + let owner_key = accounts + .get(packed.owner as usize) + .ok_or(LightSdkTypesError::InvalidInstructionData)? + .key(); + let mint_key = accounts + .get(packed.mint as usize) + .ok_or(LightSdkTypesError::InvalidInstructionData)? + .key(); + let delegate = if packed.has_delegate { + let delegate_key = accounts + .get(packed.delegate as usize) + .ok_or(LightSdkTypesError::InvalidInstructionData)? + .key(); + Some(light_compressed_account::Pubkey::from(delegate_key)) + } else { + None + }; + + let extensions = extension.map(|ext| { + vec![ExtensionStruct::CompressedOnly(CompressedOnlyExtension { + delegated_amount: ext.delegated_amount, + withheld_transfer_fee: ext.withheld_transfer_fee, + is_ata: ext.is_ata as u8, + })] + }); + + let state = extension.map_or(AccountState::Initialized, |ext| { + if ext.is_frozen { + AccountState::Frozen + } else { + AccountState::Initialized + } + }); + + let delegated_amount = extension.map_or(0, |ext| ext.delegated_amount); + + Ok(Token { + mint: light_compressed_account::Pubkey::from(mint_key), + owner: light_compressed_account::Pubkey::from(owner_key), + amount: packed.amount, + delegate, + state, + is_native: None, + delegated_amount, + close_authority: None, + account_type: TokenDataVersion::ShaFlat as u8, + extensions, + }) +} + +// ============================================================================= +// Pack impl (client-side only) +// ============================================================================= + +#[cfg(all(not(target_os = "solana"), feature = "std"))] +impl Pack for TokenDataWithSeeds +where + S: Pack, + S::Packed: AnchorDeserialize + AnchorSerialize + Clone + core::fmt::Debug, +{ + type Packed = TokenDataWithPackedSeeds; + + fn pack( + &self, + remaining_accounts: &mut PackedAccounts, + ) -> Result { + let seeds = self.seeds.pack(remaining_accounts)?; + + let owner_index = remaining_accounts + .insert_or_get(AM::pubkey_from_bytes(self.token_data.owner.to_bytes())); + + let token_data = PackedTokenData { + owner: owner_index, + amount: self.token_data.amount, + has_delegate: self.token_data.delegate.is_some(), + delegate: self + .token_data + .delegate + .map(|d| remaining_accounts.insert_or_get(AM::pubkey_from_bytes(d.to_bytes()))) + .unwrap_or(0), + mint: remaining_accounts + .insert_or_get(AM::pubkey_from_bytes(self.token_data.mint.to_bytes())), + version: TokenDataVersion::ShaFlat as u8, + }; + + let extension = self.token_data.extensions.as_ref().and_then(|exts| { + exts.iter().find_map(|ext| { + if let ExtensionStruct::CompressedOnly(co) = ext { + Some(CompressedOnlyExtensionInstructionData { + delegated_amount: co.delegated_amount, + withheld_transfer_fee: co.withheld_transfer_fee, + is_frozen: self.token_data.state == AccountState::Frozen, + compression_index: 0, + is_ata: co.is_ata != 0, + bump: 0, + owner_index, + }) + } else { + None + } + }) + }); + + Ok(TokenDataWithPackedSeeds { + seeds, + token_data, + extension, + }) + } +} + +// ============================================================================= +// Unpack impl +// ============================================================================= + +impl Unpack for TokenDataWithPackedSeeds +where + S: Unpack + AnchorSerialize + AnchorDeserialize + Clone + core::fmt::Debug, +{ + type Unpacked = TokenDataWithSeeds<>::Unpacked>; + + fn unpack(&self, remaining_accounts: &[AI]) -> Result { + let seeds = self.seeds.unpack(remaining_accounts)?; + let token_data = + unpack_token_data_from_packed(&self.token_data, &self.extension, remaining_accounts)?; + Ok(TokenDataWithSeeds { seeds, token_data }) + } +} + +// ============================================================================= +// Blanket impls: LightAccountVariantTrait / PackedLightAccountVariantTrait +// for TokenDataWithSeeds / TokenDataWithPackedSeeds +// where S implements the seed-specific helper traits. +// ============================================================================= + +impl LightAccountVariantTrait for TokenDataWithSeeds +where + S: UnpackedTokenSeeds, + S::Packed: PackedTokenSeeds, +{ + const PROGRAM_ID: [u8; 32] = S::PROGRAM_ID; + type Seeds = S; + type Data = Token; + type Packed = TokenDataWithPackedSeeds; + + fn data(&self) -> &Self::Data { + &self.token_data + } + + fn seed_vec(&self) -> Vec> { + self.seeds.seed_vec() + } + + fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; N] { + self.seeds.seed_refs_with_bump(bump_storage) + } +} + +impl PackedLightAccountVariantTrait for TokenDataWithPackedSeeds +where + S: PackedTokenSeeds + AnchorSerialize + AnchorDeserialize + Clone + core::fmt::Debug, + S::Unpacked: UnpackedTokenSeeds, +{ + type Unpacked = TokenDataWithSeeds; + + const ACCOUNT_TYPE: AccountType = AccountType::Token; + + fn bump(&self) -> u8 { + self.seeds.bump() + } + + fn unpack( + &self, + accounts: &[AI], + ) -> Result { + let seeds = self.seeds.unpack_seeds::(accounts)?; + let token_data = + unpack_token_data_from_packed(&self.token_data, &self.extension, accounts)?; + Ok(TokenDataWithSeeds { seeds, token_data }) + } + + fn seed_refs_with_bump<'a, AI: AccountInfoTrait>( + &'a self, + accounts: &'a [AI], + bump_storage: &'a [u8; 1], + ) -> Result<[&'a [u8]; N], LightSdkTypesError> { + self.seeds.seed_refs_with_bump(accounts, bump_storage) + } + + fn into_in_token_data( + &self, + tree_info: &PackedStateTreeInfo, + output_queue_index: u8, + ) -> Result { + Ok(MultiInputTokenDataWithContext { + amount: self.token_data.amount, + mint: self.token_data.mint, + owner: self.token_data.owner, + version: self.token_data.version, + has_delegate: self.token_data.has_delegate, + delegate: self.token_data.delegate, + root_index: tree_info.root_index, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: output_queue_index, + leaf_index: tree_info.leaf_index, + prove_by_index: tree_info.prove_by_index, + }, + }) + } + + fn into_in_tlv(&self) -> Result>, LightSdkTypesError> { + Ok(self + .extension + .as_ref() + .map(|ext| vec![ExtensionInstructionData::CompressedOnly(*ext)])) + } + + fn derive_owner(&self) -> [u8; 32] { + self.seeds.derive_owner() + } +} diff --git a/sdk-libs/sdk/src/interface/accounts/finalize.rs b/sdk-libs/sdk-types/src/interface/accounts/finalize.rs similarity index 52% rename from sdk-libs/sdk/src/interface/accounts/finalize.rs rename to sdk-libs/sdk-types/src/interface/accounts/finalize.rs index b16767f7cc..82a3ab335d 100644 --- a/sdk-libs/sdk/src/interface/accounts/finalize.rs +++ b/sdk-libs/sdk-types/src/interface/accounts/finalize.rs @@ -9,7 +9,7 @@ //! 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; +use light_account_checks::AccountInfoTrait; /// Trait for pre-initialization operations (mint creation). /// @@ -21,51 +21,28 @@ use solana_account_info::AccountInfo; /// mints and PDAs. /// /// # Type Parameters -/// * `'info` - The account info lifetime +/// * `AI` - AccountInfoTrait implementation (solana or pinocchio) /// * `P` - The instruction params type (from `#[instruction(params: P)]`) -pub trait LightPreInit<'info, P> { +pub trait LightPreInit { /// 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>], + remaining_accounts: &[AI], params: &P, - ) -> Result; + ) -> Result; } /// Trait for finalizing compression operations on accounts. /// /// # Type Parameters -/// * `'info` - The account info lifetime +/// * `AI` - AccountInfoTrait implementation (solana or pinocchio) /// * `P` - The instruction params type (from `#[instruction(params: P)]`) -/// -pub trait LightFinalize<'info, P> { +pub trait LightFinalize { /// 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>], + remaining_accounts: &[AI], params: &P, has_pre_init: bool, - ) -> Result<(), crate::error::LightSdkError>; + ) -> Result<(), crate::error::LightSdkTypesError>; } diff --git a/sdk-libs/sdk-types/src/interface/accounts/init_compressed_account.rs b/sdk-libs/sdk-types/src/interface/accounts/init_compressed_account.rs new file mode 100644 index 0000000000..9e3d3d9a85 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/accounts/init_compressed_account.rs @@ -0,0 +1,101 @@ +//! Helper functions for preparing compressed accounts on init. + +use alloc::vec::Vec; + +use light_account_checks::AccountInfoTrait; +use light_compressed_account::{ + address::derive_address, + instruction_data::{ + data::NewAddressParamsAssignedPacked, + with_account_info::{CompressedAccountInfo, OutAccountInfo}, + }, +}; +use light_compressible::DECOMPRESSED_PDA_DISCRIMINATOR; +use light_hasher::{errors::HasherError, sha256::Sha256BE, Hasher}; + +use crate::{error::LightSdkTypesError, instruction::PackedAddressTreeInfo}; + +/// Prepare a compressed account for a PDA during initialization. +/// +/// This function handles the common pattern of: +/// 1. Deriving the compressed address from the PDA pubkey seed +/// 2. Creating NewAddressParamsAssignedPacked for the address tree +/// 3. Building CompressedAccountInfo with hashed PDA pubkey data +/// +/// Uses `[u8; 32]` for all pubkey parameters - framework-agnostic. +#[inline(never)] +#[allow(clippy::too_many_arguments)] +pub fn prepare_compressed_account_on_init( + pda_pubkey: &[u8; 32], + address_tree_pubkey: &[u8; 32], + address_tree_info: &PackedAddressTreeInfo, + output_tree_index: u8, + assigned_account_index: u8, + program_id: &[u8; 32], + new_address_params: &mut Vec, + account_infos: &mut Vec, +) -> Result<(), HasherError> { + // Data is always the PDA pubkey bytes + let data = pda_pubkey.to_vec(); + + // Derive compressed address from PDA pubkey seed + let address = derive_address(pda_pubkey, address_tree_pubkey, program_id); + + // Create and push new address params + new_address_params.push(NewAddressParamsAssignedPacked { + seed: *pda_pubkey, + address_merkle_tree_account_index: address_tree_info.address_merkle_tree_pubkey_index, + address_queue_account_index: address_tree_info.address_queue_pubkey_index, + address_merkle_tree_root_index: address_tree_info.root_index, + assigned_to_account: true, + assigned_account_index, + }); + + // Hash the data for the compressed account + let data_hash = Sha256BE::hash(&data)?; + + // Create and push CompressedAccountInfo + account_infos.push(CompressedAccountInfo { + address: Some(address), + input: None, + output: Some(OutAccountInfo { + discriminator: DECOMPRESSED_PDA_DISCRIMINATOR, + output_merkle_tree_index: output_tree_index, + lamports: 0, + data, + data_hash, + }), + }); + + Ok(()) +} + +/// Reimburse the fee_payer for rent paid during PDA creation. +/// +/// During Anchor `init`, the fee_payer pays rent for PDA accounts. +/// This function transfers the total rent amount from the rent_sponsor +/// PDA back to the fee_payer via system program CPI. +/// +/// The rent_sponsor is a system-owned PDA, so CPI with invoke_signed +/// is required (direct lamport manipulation would fail). +pub fn reimburse_rent( + created_accounts: &[AI], + fee_payer: &AI, + rent_sponsor: &AI, + rent_sponsor_signer_seeds: &[&[u8]], +) -> Result<(), LightSdkTypesError> { + let mut total_rent: u64 = 0; + for account in created_accounts { + total_rent = total_rent + .checked_add(account.lamports()) + .ok_or(LightSdkTypesError::ConstraintViolation)?; + } + + if total_rent > 0 { + rent_sponsor + .transfer_lamports_cpi(fee_payer, total_rent, rent_sponsor_signer_seeds) + .map_err(LightSdkTypesError::AccountError)?; + } + + Ok(()) +} diff --git a/sdk-libs/sdk/src/interface/accounts/mod.rs b/sdk-libs/sdk-types/src/interface/accounts/mod.rs similarity index 85% rename from sdk-libs/sdk/src/interface/accounts/mod.rs rename to sdk-libs/sdk-types/src/interface/accounts/mod.rs index cf105f5726..3e313e9503 100644 --- a/sdk-libs/sdk/src/interface/accounts/mod.rs +++ b/sdk-libs/sdk-types/src/interface/accounts/mod.rs @@ -3,7 +3,5 @@ //! This module contains traits and functions for context struct handling, //! validation, and initialization at the accounts struct level. -#[cfg(feature = "v2")] -pub mod create_pda; pub mod finalize; pub mod init_compressed_account; diff --git a/sdk-libs/sdk-types/src/interface/cpi/account.rs b/sdk-libs/sdk-types/src/interface/cpi/account.rs new file mode 100644 index 0000000000..01dfb15612 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/cpi/account.rs @@ -0,0 +1,153 @@ +//! Generic CPI accounts trait and implementations. + +use alloc::{vec, vec::Vec}; + +use light_account_checks::{AccountInfoTrait, CpiMeta}; + +use crate::{ + cpi_accounts::v2::{CompressionCpiAccountIndex, CpiAccounts, PROGRAM_ACCOUNTS_LEN}, + cpi_context_write::CpiContextWriteAccounts, + error::LightSdkTypesError, +}; + +/// Trait for types that can provide account infos and metas for Light system program CPI. +/// +/// Generic over `AI: AccountInfoTrait` to work with both solana and pinocchio backends. +pub trait CpiAccountsTrait { + fn to_account_infos(&self) -> Vec; + fn to_account_metas(&self) -> Result, LightSdkTypesError>; + fn get_mode(&self) -> Option; +} + +/// Build `CpiMeta` vec from `CpiAccounts` (v2 mode=1). +impl<'a, AI: AccountInfoTrait + Clone> CpiAccountsTrait for CpiAccounts<'a, AI> { + fn to_account_infos(&self) -> Vec { + CpiAccounts::to_account_infos(self) + } + + fn to_account_metas(&self) -> Result, LightSdkTypesError> { + to_cpi_metas(self) + } + + fn get_mode(&self) -> Option { + Some(1) // v2 mode + } +} + +/// Build `CpiMeta` vec from `CpiContextWriteAccounts` (3-account CPI context write). +impl<'a, AI: AccountInfoTrait + Clone> CpiAccountsTrait for CpiContextWriteAccounts<'a, AI> { + fn to_account_infos(&self) -> Vec { + self.to_account_infos().to_vec() + } + + fn to_account_metas(&self) -> Result, LightSdkTypesError> { + let infos = self.to_account_info_refs(); + Ok(vec![ + CpiMeta { + pubkey: infos[0].key(), + is_signer: true, + is_writable: true, + }, + CpiMeta { + pubkey: infos[1].key(), + is_signer: true, + is_writable: false, + }, + CpiMeta { + pubkey: infos[2].key(), + is_signer: false, + is_writable: true, + }, + ]) + } + + fn get_mode(&self) -> Option { + Some(1) // v2 mode + } +} + +/// Convert `CpiAccounts` to a vec of `CpiMeta`, preserving the account layout +/// expected by the Light system program. +fn to_cpi_metas( + cpi_accounts: &CpiAccounts<'_, AI>, +) -> Result, LightSdkTypesError> { + let mut metas = + Vec::with_capacity(1 + cpi_accounts.account_infos().len() - PROGRAM_ACCOUNTS_LEN); + + metas.push(CpiMeta { + pubkey: cpi_accounts.fee_payer().key(), + is_signer: true, + is_writable: true, + }); + metas.push(CpiMeta { + pubkey: cpi_accounts.authority()?.key(), + is_signer: true, + is_writable: false, + }); + metas.push(CpiMeta { + pubkey: cpi_accounts.registered_program_pda()?.key(), + is_signer: false, + is_writable: false, + }); + metas.push(CpiMeta { + pubkey: cpi_accounts.account_compression_authority()?.key(), + is_signer: false, + is_writable: false, + }); + metas.push(CpiMeta { + pubkey: cpi_accounts.account_compression_program()?.key(), + is_signer: false, + is_writable: false, + }); + metas.push(CpiMeta { + pubkey: cpi_accounts.system_program()?.key(), + is_signer: false, + is_writable: false, + }); + + let accounts = cpi_accounts.account_infos(); + let mut index = CompressionCpiAccountIndex::SolPoolPda as usize; + + if cpi_accounts.config().sol_pool_pda { + let account = cpi_accounts.get_account_info(index)?; + metas.push(CpiMeta { + pubkey: account.key(), + is_signer: false, + is_writable: true, + }); + index += 1; + } + + if cpi_accounts.config().sol_compression_recipient { + let account = cpi_accounts.get_account_info(index)?; + metas.push(CpiMeta { + pubkey: account.key(), + is_signer: false, + is_writable: true, + }); + index += 1; + } + + if cpi_accounts.config().cpi_context { + let account = cpi_accounts.get_account_info(index)?; + metas.push(CpiMeta { + pubkey: account.key(), + is_signer: false, + is_writable: true, + }); + index += 1; + } + assert_eq!(cpi_accounts.system_accounts_end_offset(), index); + + let tree_accounts = accounts + .get(index..) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index))?; + tree_accounts.iter().for_each(|acc| { + metas.push(CpiMeta { + pubkey: acc.key(), + is_signer: acc.is_signer(), + is_writable: acc.is_writable(), + }); + }); + Ok(metas) +} diff --git a/sdk-libs/sdk-types/src/interface/cpi/create_mints.rs b/sdk-libs/sdk-types/src/interface/cpi/create_mints.rs new file mode 100644 index 0000000000..7d8c412a78 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/cpi/create_mints.rs @@ -0,0 +1,979 @@ +//! Generic CPI for creating multiple compressed mints. +//! +//! This module provides framework-agnostic mint creation via `CreateMintsCpi`, +//! generic over `AccountInfoTrait`. Account order matches the cToken program +//! expectations (see `MintActionMetaConfig::to_account_metas` for reference). +//! +//! # Flow +//! +//! - N=1 (no CPI context offset): Single CPI (create + decompress) +//! - N>1 or offset>0: 2N-1 CPIs (N-1 writes + 1 execute with decompress + N-1 decompress) + +use alloc::{vec, vec::Vec}; + +use light_account_checks::{AccountInfoTrait, CpiMeta}; +use light_compressed_account::instruction_data::{ + compressed_proof::CompressedProof, traits::LightInstructionData, +}; +use light_token_interface::{ + instructions::{ + extensions::{ExtensionInstructionData, TokenMetadataInstructionData}, + mint_action::{ + Action, CpiContext, CreateMint, DecompressMintAction, + MintActionCompressedInstructionData, MintInstructionData, + }, + }, + state::MintMetadata, + COMPRESSED_MINT_SEED, LIGHT_TOKEN_PROGRAM_ID, +}; + +use crate::error::LightSdkTypesError; + +/// Default rent payment epochs (~24 hours). +pub const DEFAULT_RENT_PAYMENT: u8 = 16; +/// Default lamports for write operations (~3 hours per write). +pub const DEFAULT_WRITE_TOP_UP: u32 = 766; + +// ============================================================================ +// Types +// ============================================================================ + +/// Parameters for a single mint within a batch creation. +/// +/// All pubkeys are `[u8; 32]` for framework independence. +/// `mint` and `compression_address` are derived internally from `mint_seed_pubkey`. +#[derive(Debug, Clone)] +pub struct SingleMintParams<'a> { + pub decimals: u8, + pub mint_authority: [u8; 32], + /// Optional mint bump. If `None`, derived from `find_mint_address(mint_seed_pubkey)`. + pub mint_bump: Option, + pub freeze_authority: Option<[u8; 32]>, + /// Mint seed pubkey (signer) for this mint. Used to derive `mint` PDA and `compression_address`. + pub mint_seed_pubkey: [u8; 32], + /// Optional authority seeds for PDA signing. + pub authority_seeds: Option<&'a [&'a [u8]]>, + /// Optional mint signer seeds for PDA signing. + pub mint_signer_seeds: Option<&'a [&'a [u8]]>, + /// Optional token metadata for the mint (reference to avoid stack overflow). + pub token_metadata: Option<&'a TokenMetadataInstructionData>, +} + +/// Parameters for creating one or more compressed mints with decompression. +/// +/// Creates N compressed mints and decompresses all to Solana Mint accounts. +/// Uses CPI context pattern when N > 1 for efficiency. +#[derive(Debug, Clone)] +pub struct CreateMintsParams<'a> { + /// Parameters for each mint to create. + pub mints: &'a [SingleMintParams<'a>], + /// Single proof covering all new addresses. + pub proof: CompressedProof, + /// Root index for the address merkle tree (shared by all mints in batch). + pub address_merkle_tree_root_index: u16, + /// Rent payment in epochs for the Mint account (must be 0 or >= 2). + /// Default: 16 (~24 hours). + pub rent_payment: u8, + /// Lamports allocated for future write operations. + /// Default: 766 (~3 hours per write). + pub write_top_up: u32, + /// Offset for assigned_account_index when sharing CPI context with other accounts. + /// When creating mints alongside PDAs, this offset should be set to the number of + /// PDAs already written to the CPI context. + /// Default: 0 (no offset). + pub cpi_context_offset: u8, + /// Index of the output queue in tree accounts. + /// Default: 0. + pub output_queue_index: u8, + /// Index of the address merkle tree in tree accounts. + /// Default: 1. + pub address_tree_index: u8, + /// Index of the state merkle tree in tree accounts. + /// Required for decompress operations (discriminator validation). + /// Default: 2. + pub state_tree_index: u8, + /// Base leaf index from the output queue (required when N > 1). + /// Read from the queue's batch_metadata.next_index before any CPIs. + /// For N=1 with offset=0, pass 0. + pub base_leaf_index: u32, +} + +#[cfg(feature = "cpi-context")] +impl<'a> CreateMintsParams<'a> { + /// Create params from proof data and CPI accounts. + /// + /// Extracts tree indices and computes base_leaf_index automatically (only for N > 1 mints). + /// Uses default values for rent_payment, write_top_up, and cpi_context_offset. + pub fn from_proof( + mints: &'a [SingleMintParams<'a>], + proof_data: &crate::interface::CreateAccountsProof, + cpi_accounts: &crate::cpi_accounts::v2::CpiAccounts<'_, AI>, + ) -> Result { + let proof = proof_data + .proof + .0 + .ok_or(LightSdkTypesError::InvalidInstructionData)?; + + let state_tree_index = proof_data + .state_tree_index + .ok_or(LightSdkTypesError::InvalidInstructionData)?; + + let output_queue_index = proof_data.output_state_tree_index; + + // Only read base_leaf_index when there are multiple mints (needed for decompress indexing) + let base_leaf_index = if mints.len() > 1 { + let output_queue = cpi_accounts.get_tree_account_info(output_queue_index as usize)?; + get_output_queue_next_index(output_queue)? + } else { + 0 + }; + + Ok(Self { + mints, + proof, + address_merkle_tree_root_index: proof_data.address_tree_info.root_index, + rent_payment: DEFAULT_RENT_PAYMENT, + write_top_up: DEFAULT_WRITE_TOP_UP, + cpi_context_offset: 0, + output_queue_index, + address_tree_index: proof_data + .address_tree_info + .address_merkle_tree_pubkey_index, + state_tree_index, + base_leaf_index, + }) + } +} + +/// Infrastructure accounts needed for mint creation CPI. +/// +/// These accounts are passed from the user's Accounts struct. +pub struct CreateMintsStaticAccounts<'a, AI: AccountInfoTrait + Clone> { + /// Fee payer for the transaction. + pub fee_payer: &'a AI, + /// CompressibleConfig account for the light-token program. + pub compressible_config: &'a AI, + /// Rent sponsor PDA. + pub rent_sponsor: &'a AI, + /// CPI authority PDA for signing. + pub cpi_authority: &'a AI, +} + +/// CPI struct for creating multiple compressed mints. +/// +/// Generic over `AccountInfoTrait` to work with both solana and pinocchio backends. +/// Uses named account fields for clarity and safety. +pub struct CreateMintsCpi<'a, AI: AccountInfoTrait + Clone> { + /// Mint seed accounts (signers) - one per mint. + pub mint_seed_accounts: &'a [AI], + /// Fee payer (also used as authority). + pub payer: &'a AI, + /// Address tree for new mint addresses. + pub address_tree: &'a AI, + /// Output queue for compressed accounts. + pub output_queue: &'a AI, + /// State merkle tree (required for decompress discriminator validation). + pub state_merkle_tree: &'a AI, + /// CompressibleConfig account. + pub compressible_config: &'a AI, + /// Mint PDA accounts (writable) - one per mint. + pub mints: &'a [AI], + /// Rent sponsor PDA. + pub rent_sponsor: &'a AI, + /// Light system program. + pub light_system_program: &'a AI, + /// CPI authority PDA. + pub cpi_authority_pda: &'a AI, + /// Registered program PDA. + pub registered_program_pda: &'a AI, + /// Account compression authority. + pub account_compression_authority: &'a AI, + /// Account compression program. + pub account_compression_program: &'a AI, + /// System program. + pub system_program: &'a AI, + /// CPI context account. + pub cpi_context_account: &'a AI, + /// Parameters. + pub params: CreateMintsParams<'a>, +} + +impl<'a, AI: AccountInfoTrait + Clone> CreateMintsCpi<'a, AI> { + /// Validate that the struct is properly constructed. + #[inline(never)] + fn validate(&self) -> Result<(), LightSdkTypesError> { + let n = self.params.mints.len(); + if n == 0 { + return Err(LightSdkTypesError::InvalidInstructionData); + } + if self.mint_seed_accounts.len() != n { + return Err(LightSdkTypesError::InvalidInstructionData); + } + if self.mints.len() != n { + return Err(LightSdkTypesError::InvalidInstructionData); + } + Ok(()) + } + + /// Execute all CPIs to create and decompress all mints. + #[inline(never)] + pub fn invoke(self) -> Result<(), LightSdkTypesError> { + self.validate()?; + let n = self.params.mints.len(); + + // Use single mint path only when: + // - N=1 AND + // - No CPI context offset (no PDAs were written to CPI context first) + if n == 1 && self.params.cpi_context_offset == 0 { + self.invoke_single_mint() + } else { + self.invoke_multiple_mints() + } + } + + /// Handle the single mint case: create + decompress in one CPI. + #[inline(never)] + fn invoke_single_mint(self) -> Result<(), LightSdkTypesError> { + let mint_params = &self.params.mints[0]; + let (mint, bump) = get_mint_and_bump::(mint_params); + + let mint_data = + build_mint_instruction_data(mint_params, &self.mint_seed_accounts[0].key(), mint, bump); + + let decompress_action = DecompressMintAction { + rent_payment: self.params.rent_payment, + write_top_up: self.params.write_top_up, + }; + + let instruction_data = MintActionCompressedInstructionData::new_mint( + self.params.address_merkle_tree_root_index, + self.params.proof, + mint_data, + ) + .with_decompress_mint(decompress_action); + + let ix_data = instruction_data + .data() + .map_err(|_| LightSdkTypesError::Borsh)?; + + let (metas, account_infos) = self.build_mint_action(0, true, true, false); + + self.invoke_mint_action_raw(&ix_data, &account_infos, &metas, 0) + } + + /// Handle the multiple mints case: N-1 writes + 1 execute + N-1 decompress. + #[inline(never)] + fn invoke_multiple_mints(self) -> Result<(), LightSdkTypesError> { + let n = self.params.mints.len(); + let base_leaf_index = self.params.base_leaf_index; + + let decompress_action = DecompressMintAction { + rent_payment: self.params.rent_payment, + write_top_up: self.params.write_top_up, + }; + + // Write mints 0..N-2 to CPI context + for i in 0..(n - 1) { + self.invoke_cpi_write(i)?; + } + + // Execute: create last mint + decompress it + self.invoke_execute(n - 1, &decompress_action)?; + + // Decompress remaining mints (0..N-2) + for i in 0..(n - 1) { + self.invoke_decompress(i, base_leaf_index, &decompress_action)?; + } + + Ok(()) + } + + /// Invoke a CPI write instruction for a single mint. + #[inline(never)] + fn invoke_cpi_write(&self, index: usize) -> Result<(), LightSdkTypesError> { + let mint_params = &self.params.mints[index]; + let offset = self.params.cpi_context_offset; + let (mint, bump) = get_mint_and_bump::(mint_params); + + let cpi_context = CpiContext { + set_context: index > 0 || offset > 0, + first_set_context: index == 0 && offset == 0, + in_tree_index: self.params.address_tree_index, + in_queue_index: self.params.output_queue_index, + out_queue_index: self.params.output_queue_index, + token_out_queue_index: 0, + assigned_account_index: offset + index as u8, + read_only_address_trees: [0; 4], + address_tree_pubkey: self.address_tree.key(), + }; + + let mint_data = build_mint_instruction_data( + mint_params, + &self.mint_seed_accounts[index].key(), + mint, + bump, + ); + + let instruction_data = MintActionCompressedInstructionData::new_mint_write_to_cpi_context( + self.params.address_merkle_tree_root_index, + mint_data, + cpi_context, + ); + + let ix_data = instruction_data + .data() + .map_err(|_| LightSdkTypesError::Borsh)?; + + // CPI write account order: + // [0]: light_system_program + // [1]: mint_signer + // [2]: authority (payer) + // [3]: fee_payer (payer) + // [4]: cpi_authority_pda + // [5]: cpi_context + let metas = vec![ + CpiMeta { + pubkey: self.light_system_program.key(), + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: self.mint_seed_accounts[index].key(), + is_signer: true, + is_writable: false, + }, + CpiMeta { + pubkey: self.payer.key(), + is_signer: true, + is_writable: false, + }, + CpiMeta { + pubkey: self.payer.key(), + is_signer: true, + is_writable: true, + }, + CpiMeta { + pubkey: self.cpi_authority_pda.key(), + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: self.cpi_context_account.key(), + is_signer: false, + is_writable: true, + }, + ]; + + let account_infos = vec![ + self.light_system_program.clone(), + self.mint_seed_accounts[index].clone(), + self.payer.clone(), + self.payer.clone(), + self.cpi_authority_pda.clone(), + self.cpi_context_account.clone(), + ]; + + self.invoke_mint_action_raw(&ix_data, &account_infos, &metas, index) + } + + /// Invoke the execute instruction (create last mint + decompress). + #[inline(never)] + fn invoke_execute( + &self, + last_idx: usize, + decompress_action: &DecompressMintAction, + ) -> Result<(), LightSdkTypesError> { + let mint_params = &self.params.mints[last_idx]; + let offset = self.params.cpi_context_offset; + let (mint, bump) = get_mint_and_bump::(mint_params); + + let mint_data = build_mint_instruction_data( + mint_params, + &self.mint_seed_accounts[last_idx].key(), + mint, + bump, + ); + + let instruction_data = MintActionCompressedInstructionData { + leaf_index: 0, + prove_by_index: false, + root_index: self.params.address_merkle_tree_root_index, + max_top_up: 0, + create_mint: Some(CreateMint::default()), + actions: vec![Action::DecompressMint(*decompress_action)], + proof: Some(self.params.proof), + cpi_context: Some(CpiContext { + set_context: false, + first_set_context: false, + in_tree_index: self.params.address_tree_index, + in_queue_index: self.params.address_tree_index, + out_queue_index: self.params.output_queue_index, + token_out_queue_index: 0, + assigned_account_index: offset + last_idx as u8, + read_only_address_trees: [0; 4], + address_tree_pubkey: self.address_tree.key(), + }), + mint: Some(mint_data), + }; + + let ix_data = instruction_data + .data() + .map_err(|_| LightSdkTypesError::Borsh)?; + + let (metas, account_infos) = self.build_mint_action(last_idx, true, true, true); + + self.invoke_mint_action_raw(&ix_data, &account_infos, &metas, last_idx) + } + + /// Invoke decompress for a single mint. + #[inline(never)] + fn invoke_decompress( + &self, + index: usize, + base_leaf_index: u32, + decompress_action: &DecompressMintAction, + ) -> Result<(), LightSdkTypesError> { + let mint_params = &self.params.mints[index]; + let (mint, bump) = get_mint_and_bump::(mint_params); + + let mint_data = build_mint_instruction_data( + mint_params, + &self.mint_seed_accounts[index].key(), + mint, + bump, + ); + + let instruction_data = MintActionCompressedInstructionData { + leaf_index: base_leaf_index + self.params.cpi_context_offset as u32 + index as u32, + prove_by_index: true, + root_index: 0, + max_top_up: 0, + create_mint: None, + actions: vec![Action::DecompressMint(*decompress_action)], + proof: None, + cpi_context: None, + mint: Some(mint_data), + }; + + let ix_data = instruction_data + .data() + .map_err(|_| LightSdkTypesError::Borsh)?; + + let (metas, account_infos) = self.build_decompress_action(index); + + self.invoke_mint_action_raw(&ix_data, &account_infos, &metas, index) + } + + /// Low-level invoke: build signer seeds from mint params and call CPI. + #[inline(never)] + fn invoke_mint_action_raw( + &self, + ix_data: &[u8], + account_infos: &[AI], + metas: &[CpiMeta], + mint_index: usize, + ) -> Result<(), LightSdkTypesError> { + let mint_params = &self.params.mints[mint_index]; + + // Build signer seeds - pack present seeds at start of array + let mut seeds: [&[&[u8]]; 2] = [&[], &[]]; + let mut num_signers = 0; + if let Some(s) = mint_params.mint_signer_seeds { + seeds[num_signers] = s; + num_signers += 1; + } + if let Some(s) = mint_params.authority_seeds { + seeds[num_signers] = s; + num_signers += 1; + } + + AI::invoke_cpi( + &LIGHT_TOKEN_PROGRAM_ID, + ix_data, + metas, + account_infos, + &seeds[..num_signers], + ) + .map_err(|_| LightSdkTypesError::CpiFailed) + } + + /// Build matched account metas and infos for a full mint action CPI. + /// + /// Returns `(metas, infos)` in identical order so pinocchio's 1:1 + /// positional CPI requirement is satisfied without runtime reordering. + /// + /// Order matches `MintActionMetaConfig::to_account_metas`: + /// light_system_program, [mint_signer], authority, [compressible_config], + /// [mint], [rent_sponsor], fee_payer, cpi_authority_pda, registered_program_pda, + /// account_compression_authority, account_compression_program, system_program, + /// [cpi_context], output_queue, tree_pubkey, [input_queue] + #[inline(never)] + fn build_mint_action( + &self, + mint_index: usize, + has_input_queue: bool, + has_compressible: bool, + has_cpi_context: bool, + ) -> (Vec, Vec) { + let mut metas = Vec::with_capacity(17); + let mut infos = Vec::with_capacity(17); + + // light_system_program + metas.push(CpiMeta { + pubkey: self.light_system_program.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.light_system_program.clone()); + + // mint_signer (always present for create_mint, must sign) + metas.push(CpiMeta { + pubkey: self.mint_seed_accounts[mint_index].key(), + is_signer: true, + is_writable: false, + }); + infos.push(self.mint_seed_accounts[mint_index].clone()); + + // authority (payer is authority) + metas.push(CpiMeta { + pubkey: self.payer.key(), + is_signer: true, + is_writable: false, + }); + infos.push(self.payer.clone()); + + if has_compressible { + // compressible_config + metas.push(CpiMeta { + pubkey: self.compressible_config.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.compressible_config.clone()); + + // mint PDA (writable) + metas.push(CpiMeta { + pubkey: self.mints[mint_index].key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.mints[mint_index].clone()); + + // rent_sponsor (writable) + metas.push(CpiMeta { + pubkey: self.rent_sponsor.key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.rent_sponsor.clone()); + } + + // fee_payer (signer, writable) + metas.push(CpiMeta { + pubkey: self.payer.key(), + is_signer: true, + is_writable: true, + }); + infos.push(self.payer.clone()); + + // cpi_authority_pda + metas.push(CpiMeta { + pubkey: self.cpi_authority_pda.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.cpi_authority_pda.clone()); + + // registered_program_pda + metas.push(CpiMeta { + pubkey: self.registered_program_pda.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.registered_program_pda.clone()); + + // account_compression_authority + metas.push(CpiMeta { + pubkey: self.account_compression_authority.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.account_compression_authority.clone()); + + // account_compression_program + metas.push(CpiMeta { + pubkey: self.account_compression_program.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.account_compression_program.clone()); + + // system_program + metas.push(CpiMeta { + pubkey: self.system_program.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.system_program.clone()); + + // cpi_context (optional) + if has_cpi_context { + metas.push(CpiMeta { + pubkey: self.cpi_context_account.key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.cpi_context_account.clone()); + } + + // output_queue (writable) + metas.push(CpiMeta { + pubkey: self.output_queue.key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.output_queue.clone()); + + // tree_pubkey (address_tree for create_mint) + metas.push(CpiMeta { + pubkey: self.address_tree.key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.address_tree.clone()); + + // input_queue (optional, same as output_queue for create_mint) + if has_input_queue { + metas.push(CpiMeta { + pubkey: self.output_queue.key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.output_queue.clone()); + } + + (metas, infos) + } + + /// Build matched account metas and infos for a decompress CPI. + /// + /// For prove_by_index, tree_pubkey must be state_merkle_tree for discriminator validation. + #[inline(never)] + fn build_decompress_action(&self, mint_index: usize) -> (Vec, Vec) { + let mut metas = Vec::with_capacity(14); + let mut infos = Vec::with_capacity(14); + + // light_system_program + metas.push(CpiMeta { + pubkey: self.light_system_program.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.light_system_program.clone()); + + // No mint_signer for decompress + + // authority (payer is authority, signer) + metas.push(CpiMeta { + pubkey: self.payer.key(), + is_signer: true, + is_writable: false, + }); + infos.push(self.payer.clone()); + + // compressible_config + metas.push(CpiMeta { + pubkey: self.compressible_config.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.compressible_config.clone()); + + // mint PDA (writable) + metas.push(CpiMeta { + pubkey: self.mints[mint_index].key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.mints[mint_index].clone()); + + // rent_sponsor (writable) + metas.push(CpiMeta { + pubkey: self.rent_sponsor.key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.rent_sponsor.clone()); + + // fee_payer (signer, writable) + metas.push(CpiMeta { + pubkey: self.payer.key(), + is_signer: true, + is_writable: true, + }); + infos.push(self.payer.clone()); + + // cpi_authority_pda + metas.push(CpiMeta { + pubkey: self.cpi_authority_pda.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.cpi_authority_pda.clone()); + + // registered_program_pda + metas.push(CpiMeta { + pubkey: self.registered_program_pda.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.registered_program_pda.clone()); + + // account_compression_authority + metas.push(CpiMeta { + pubkey: self.account_compression_authority.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.account_compression_authority.clone()); + + // account_compression_program + metas.push(CpiMeta { + pubkey: self.account_compression_program.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.account_compression_program.clone()); + + // system_program + metas.push(CpiMeta { + pubkey: self.system_program.key(), + is_signer: false, + is_writable: false, + }); + infos.push(self.system_program.clone()); + + // No cpi_context for decompress + + // output_queue (writable) + metas.push(CpiMeta { + pubkey: self.output_queue.key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.output_queue.clone()); + + // tree_pubkey = state_merkle_tree for prove_by_index discriminator check + metas.push(CpiMeta { + pubkey: self.state_merkle_tree.key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.state_merkle_tree.clone()); + + // input_queue = output_queue + metas.push(CpiMeta { + pubkey: self.output_queue.key(), + is_signer: false, + is_writable: true, + }); + infos.push(self.output_queue.clone()); + + (metas, infos) + } +} + +// ============================================================================ +// Helpers +// ============================================================================ + +/// Get mint PDA and bump, deriving mint always and bump if None. +#[inline(never)] +fn get_mint_and_bump(params: &SingleMintParams) -> ([u8; 32], u8) { + let (mint, derived_bump) = find_mint_address::(¶ms.mint_seed_pubkey); + let bump = params.mint_bump.unwrap_or(derived_bump); + (mint, bump) +} + +/// Build `MintInstructionData` for a single mint. +/// +/// `mint` and `bump` are derived externally from `mint_seed_pubkey` using `get_mint_and_bump`. +#[inline(never)] +fn build_mint_instruction_data( + mint_params: &SingleMintParams<'_>, + mint_signer: &[u8; 32], + mint: [u8; 32], + bump: u8, +) -> MintInstructionData { + let extensions = mint_params + .token_metadata + .cloned() + .map(|metadata| vec![ExtensionInstructionData::TokenMetadata(metadata)]); + + MintInstructionData { + supply: 0, + decimals: mint_params.decimals, + metadata: MintMetadata { + version: 3, + mint: mint.into(), + mint_decompressed: false, + mint_signer: *mint_signer, + bump, + }, + mint_authority: Some(mint_params.mint_authority.into()), + freeze_authority: mint_params.freeze_authority.map(|a| a.into()), + extensions, + } +} + +/// Find the mint PDA address for a given mint seed. +/// +/// Generic over `AccountInfoTrait` to use the correct backend for PDA derivation. +/// Returns `([u8; 32], u8)` -- the PDA address bytes and bump. +pub fn find_mint_address(mint_seed: &[u8; 32]) -> ([u8; 32], u8) { + AI::find_program_address( + &[COMPRESSED_MINT_SEED, mint_seed.as_ref()], + &LIGHT_TOKEN_PROGRAM_ID, + ) +} + +/// Derive the compressed mint address from a mint seed and address tree pubkey. +/// +/// This computes `derive_address(find_mint_address(mint_seed).0, address_tree, LIGHT_TOKEN_PROGRAM_ID)`. +pub fn derive_mint_compressed_address( + mint_seed: &[u8; 32], + address_tree_pubkey: &[u8; 32], +) -> [u8; 32] { + light_compressed_account::address::derive_address( + &find_mint_address::(mint_seed).0, + address_tree_pubkey, + &LIGHT_TOKEN_PROGRAM_ID, + ) +} + +/// Read the next_index from a batched output queue account. +/// +/// Offset 288 = 8 (discriminator) + 232 (QueueMetadata) + 48 (6 x u64 in QueueBatches). +/// This reads the raw bytes to avoid depending on `light-batched-merkle-tree`. +pub fn get_output_queue_next_index( + queue: &AI, +) -> Result { + const NEXT_INDEX_OFFSET: usize = 288; + let data = queue + .try_borrow_data() + .map_err(LightSdkTypesError::AccountError)?; + if data.len() < NEXT_INDEX_OFFSET + 8 { + return Err(LightSdkTypesError::AccountDataTooSmall); + } + let next_index = u64::from_le_bytes( + data[NEXT_INDEX_OFFSET..NEXT_INDEX_OFFSET + 8] + .try_into() + .unwrap(), + ); + Ok(next_index as u32) +} + +// ============================================================================ +// High-level CreateMints API +// ============================================================================ + +/// High-level struct for creating compressed mints. +/// +/// Consolidates proof parsing, tree account resolution, and CPI invocation into +/// a single `.invoke()` call. This is the recommended API for creating mints. +/// +/// # Example +/// +/// ```rust,ignore +/// CreateMints { +/// mints: &sdk_mints, +/// proof_data: ¶ms.create_accounts_proof, +/// mint_seed_accounts, +/// mint_accounts, +/// static_accounts: CreateMintsStaticAccounts { ... }, +/// cpi_context_offset: 0, +/// } +/// .invoke(&cpi_accounts)?; +/// ``` +#[cfg(feature = "cpi-context")] +pub struct CreateMints<'a, AI: AccountInfoTrait + Clone> { + /// Per-mint parameters. + pub mints: &'a [SingleMintParams<'a>], + /// Proof data containing tree indices, proof, etc. + pub proof_data: &'a crate::interface::CreateAccountsProof, + /// Mint seed accounts (signers) - one per mint. + pub mint_seed_accounts: &'a [AI], + /// Mint PDA accounts (writable) - one per mint. + pub mint_accounts: &'a [AI], + /// Infrastructure accounts (payer, config, rent_sponsor, cpi_authority). + pub static_accounts: CreateMintsStaticAccounts<'a, AI>, + /// Offset for assigned_account_index when sharing CPI context with other accounts. + /// When creating mints alongside PDAs, this should be the number of PDAs already + /// written to the CPI context. Default: 0. + pub cpi_context_offset: u8, +} + +#[cfg(feature = "cpi-context")] +impl<'a, AI: AccountInfoTrait + Clone> CreateMints<'a, AI> { + /// Execute mint creation by: + /// 1. Building CreateMintsParams from proof_data + /// 2. Resolving tree accounts from cpi_accounts + /// 3. Invoking CreateMintsCpi + pub fn invoke( + self, + cpi_accounts: &crate::cpi_accounts::v2::CpiAccounts<'_, AI>, + ) -> Result<(), LightSdkTypesError> { + let mut params = CreateMintsParams::from_proof(self.mints, self.proof_data, cpi_accounts)?; + params.cpi_context_offset = self.cpi_context_offset; + + invoke_create_mints( + self.mint_seed_accounts, + self.mint_accounts, + params, + self.static_accounts, + cpi_accounts, + ) + } +} + +/// Convenience function that extracts accounts from CpiAccounts and invokes CreateMintsCpi. +/// +/// For new code, prefer using [`CreateMints`] with `.invoke()` instead. +#[cfg(feature = "cpi-context")] +fn invoke_create_mints<'a, AI: AccountInfoTrait + Clone>( + mint_seed_accounts: &'a [AI], + mint_accounts: &'a [AI], + params: CreateMintsParams<'a>, + infra: CreateMintsStaticAccounts<'a, AI>, + cpi_accounts: &crate::cpi_accounts::v2::CpiAccounts<'_, AI>, +) -> Result<(), LightSdkTypesError> { + let output_queue = cpi_accounts + .get_tree_account_info(params.output_queue_index as usize)? + .clone(); + let state_merkle_tree = cpi_accounts + .get_tree_account_info(params.state_tree_index as usize)? + .clone(); + let address_tree = cpi_accounts + .get_tree_account_info(params.address_tree_index as usize)? + .clone(); + + CreateMintsCpi { + mint_seed_accounts, + payer: infra.fee_payer, + address_tree: &address_tree, + output_queue: &output_queue, + state_merkle_tree: &state_merkle_tree, + compressible_config: infra.compressible_config, + mints: mint_accounts, + rent_sponsor: infra.rent_sponsor, + light_system_program: cpi_accounts.light_system_program()?, + cpi_authority_pda: infra.cpi_authority, + registered_program_pda: cpi_accounts.registered_program_pda()?, + account_compression_authority: cpi_accounts.account_compression_authority()?, + account_compression_program: cpi_accounts.account_compression_program()?, + system_program: cpi_accounts.system_program()?, + cpi_context_account: cpi_accounts.cpi_context()?, + params, + } + .invoke() +} diff --git a/sdk-libs/sdk-types/src/interface/cpi/create_token_accounts.rs b/sdk-libs/sdk-types/src/interface/cpi/create_token_accounts.rs new file mode 100644 index 0000000000..1d12ac1ac6 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/cpi/create_token_accounts.rs @@ -0,0 +1,441 @@ +//! Generic CPI builders for creating CToken accounts and ATAs. +//! +//! Provides `CreateTokenAccountCpi` and `CreateTokenAtaCpi`, both generic over +//! `AccountInfoTrait` so they work with both `solana_account_info::AccountInfo` +//! and `pinocchio::account_info::AccountInfo`. + +use alloc::{vec, vec::Vec}; + +use borsh::BorshSerialize; +use light_account_checks::{AccountInfoTrait, CpiMeta}; +use light_token_interface::{ + instructions::{ + create_associated_token_account::CreateAssociatedTokenAccountInstructionData, + create_token_account::CreateTokenAccountInstructionData, + extensions::{CompressToPubkey, CompressibleExtensionInstructionData}, + }, + LIGHT_TOKEN_PROGRAM_ID, +}; + +use crate::error::LightSdkTypesError; + +/// Discriminator for `InitializeAccount3` (create token account). +const CREATE_TOKEN_ACCOUNT_DISCRIMINATOR: u8 = 18; +/// Discriminator for `CreateAssociatedTokenAccount`. +const CREATE_ATA_DISCRIMINATOR: u8 = 100; +/// Discriminator for `CreateAssociatedTokenAccountIdempotent`. +const CREATE_ATA_IDEMPOTENT_DISCRIMINATOR: u8 = 102; + +/// Default rent payment epochs (~24 hours). +const DEFAULT_PRE_PAY_NUM_EPOCHS: u8 = 16; +/// Default lamports for write operations (~3 hours per write). +const DEFAULT_LAMPORTS_PER_WRITE: u32 = 766; +/// Default token account version (ShaFlat = 3). +const DEFAULT_TOKEN_ACCOUNT_VERSION: u8 = 3; + +// ============================================================================ +// derive_associated_token_account +// ============================================================================ + +/// Derive the associated token account address for a given owner and mint. +/// +/// Returns `([u8; 32], u8)` -- the ATA address and bump seed. +pub fn derive_associated_token_account( + owner: &[u8; 32], + mint: &[u8; 32], +) -> ([u8; 32], u8) { + AI::find_program_address( + &[ + owner.as_ref(), + LIGHT_TOKEN_PROGRAM_ID.as_ref(), + mint.as_ref(), + ], + &LIGHT_TOKEN_PROGRAM_ID, + ) +} + +// ============================================================================ +// CreateTokenAccountCpi +// ============================================================================ + +/// CPI builder for creating CToken accounts (vaults). +/// +/// Generic over `AccountInfoTrait` for framework independence. +/// +/// # Example +/// ```rust,ignore +/// CreateTokenAccountCpi { +/// payer: &ctx.accounts.payer, +/// account: &ctx.accounts.vault, +/// mint: &ctx.accounts.mint, +/// owner: ctx.accounts.vault_authority.key(), +/// } +/// .rent_free( +/// &ctx.accounts.ctoken_config, +/// &ctx.accounts.rent_sponsor, +/// &ctx.accounts.system_program, +/// &crate::ID.to_bytes(), +/// ) +/// .invoke_signed(vault_seeds)?; +/// ``` +pub struct CreateTokenAccountCpi<'a, AI: AccountInfoTrait + Clone> { + pub payer: &'a AI, + pub account: &'a AI, + pub mint: &'a AI, + pub owner: [u8; 32], +} + +impl<'a, AI: AccountInfoTrait + Clone> CreateTokenAccountCpi<'a, AI> { + /// 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: &'a AI, + sponsor: &'a AI, + system_program: &'a AI, + program_id: &[u8; 32], + ) -> CreateTokenAccountRentFreeCpi<'a, AI> { + CreateTokenAccountRentFreeCpi { + base: self, + config, + sponsor, + system_program, + program_id: *program_id, + } + } +} + +/// Rent-free enabled CToken account creation CPI. +pub struct CreateTokenAccountRentFreeCpi<'a, AI: AccountInfoTrait + Clone> { + base: CreateTokenAccountCpi<'a, AI>, + config: &'a AI, + sponsor: &'a AI, + system_program: &'a AI, + program_id: [u8; 32], +} + +impl<'a, AI: AccountInfoTrait + Clone> CreateTokenAccountRentFreeCpi<'a, AI> { + /// Invoke CPI for non-program-owned accounts. + pub fn invoke(self) -> Result<(), LightSdkTypesError> { + let (data, metas, account_infos) = self.build_instruction_inner(None)?; + AI::invoke_cpi(&LIGHT_TOKEN_PROGRAM_ID, &data, &metas, &account_infos, &[]) + .map_err(|_| LightSdkTypesError::CpiFailed) + } + + /// 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<(), LightSdkTypesError> { + // 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, + seeds: seed_vecs, + }; + + let (data, metas, account_infos) = self.build_instruction_inner(Some(compress_to))?; + AI::invoke_cpi( + &LIGHT_TOKEN_PROGRAM_ID, + &data, + &metas, + &account_infos, + &[seeds], + ) + .map_err(|_| LightSdkTypesError::CpiFailed) + } + + /// Build instruction data, account metas, and account infos. + #[allow(clippy::type_complexity)] + fn build_instruction_inner( + &self, + compress_to: Option, + ) -> Result<(Vec, Vec, Vec), LightSdkTypesError> { + let instruction_data = CreateTokenAccountInstructionData { + owner: self.base.owner.into(), + compressible_config: Some(CompressibleExtensionInstructionData { + token_account_version: DEFAULT_TOKEN_ACCOUNT_VERSION, + rent_payment: DEFAULT_PRE_PAY_NUM_EPOCHS, + compression_only: 0, // false + write_top_up: DEFAULT_LAMPORTS_PER_WRITE, + compress_to_account_pubkey: compress_to, + }), + }; + + let mut data = Vec::new(); + data.push(CREATE_TOKEN_ACCOUNT_DISCRIMINATOR); + instruction_data + .serialize(&mut data) + .map_err(|_| LightSdkTypesError::Borsh)?; + + // Account order matches the cToken program: + // [0] account (signer, writable) + // [1] mint (readonly) + // [2] payer (signer, writable) + // [3] compressible_config (readonly) + // [4] system_program (readonly) + // [5] rent_sponsor (writable) + let metas = vec![ + CpiMeta { + pubkey: self.base.account.key(), + is_signer: true, + is_writable: true, + }, + CpiMeta { + pubkey: self.base.mint.key(), + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: self.base.payer.key(), + is_signer: true, + is_writable: true, + }, + CpiMeta { + pubkey: self.config.key(), + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: self.system_program.key(), + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: self.sponsor.key(), + is_signer: false, + is_writable: true, + }, + ]; + + let account_infos = vec![ + self.base.account.clone(), + self.base.mint.clone(), + self.base.payer.clone(), + self.config.clone(), + self.system_program.clone(), + self.sponsor.clone(), + ]; + + Ok((data, metas, account_infos)) + } +} + +// ============================================================================ +// CreateTokenAtaCpi +// ============================================================================ + +/// CPI builder for creating CToken ATAs. +/// +/// Generic over `AccountInfoTrait` for framework independence. +/// +/// # Example - Rent-free ATA (idempotent) +/// ```rust,ignore +/// CreateTokenAtaCpi { +/// payer: &ctx.accounts.payer, +/// owner: &ctx.accounts.owner, +/// mint: &ctx.accounts.mint, +/// ata: &ctx.accounts.user_ata, +/// bump: params.user_ata_bump, +/// } +/// .idempotent() +/// .rent_free( +/// &ctx.accounts.ctoken_config, +/// &ctx.accounts.rent_sponsor, +/// &ctx.accounts.system_program, +/// ) +/// .invoke()?; +/// ``` +pub struct CreateTokenAtaCpi<'a, AI: AccountInfoTrait + Clone> { + pub payer: &'a AI, + pub owner: &'a AI, + pub mint: &'a AI, + pub ata: &'a AI, + pub bump: u8, +} + +impl<'a, AI: AccountInfoTrait + Clone> CreateTokenAtaCpi<'a, AI> { + /// Make this an idempotent create (won't fail if ATA already exists). + pub fn idempotent(self) -> CreateTokenAtaCpiIdempotent<'a, AI> { + CreateTokenAtaCpiIdempotent { base: self } + } + + /// Enable rent-free mode with compressible config. + pub fn rent_free( + self, + config: &'a AI, + sponsor: &'a AI, + system_program: &'a AI, + ) -> CreateTokenAtaRentFreeCpi<'a, AI> { + CreateTokenAtaRentFreeCpi { + payer: self.payer, + owner: self.owner, + mint: self.mint, + ata: self.ata, + bump: self.bump, + idempotent: false, + config, + sponsor, + system_program, + } + } +} + +/// Idempotent ATA creation (intermediate type). +pub struct CreateTokenAtaCpiIdempotent<'a, AI: AccountInfoTrait + Clone> { + base: CreateTokenAtaCpi<'a, AI>, +} + +impl<'a, AI: AccountInfoTrait + Clone> CreateTokenAtaCpiIdempotent<'a, AI> { + /// Enable rent-free mode with compressible config. + pub fn rent_free( + self, + config: &'a AI, + sponsor: &'a AI, + system_program: &'a AI, + ) -> CreateTokenAtaRentFreeCpi<'a, AI> { + CreateTokenAtaRentFreeCpi { + 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, + } + } +} + +/// Rent-free enabled CToken ATA creation CPI. +pub struct CreateTokenAtaRentFreeCpi<'a, AI: AccountInfoTrait + Clone> { + payer: &'a AI, + owner: &'a AI, + mint: &'a AI, + ata: &'a AI, + bump: u8, + idempotent: bool, + config: &'a AI, + sponsor: &'a AI, + system_program: &'a AI, +} + +impl<'a, AI: AccountInfoTrait + Clone> CreateTokenAtaRentFreeCpi<'a, AI> { + /// Invoke CPI. + pub fn invoke(self) -> Result<(), LightSdkTypesError> { + let (data, metas, account_infos) = self.build_instruction_inner()?; + AI::invoke_cpi(&LIGHT_TOKEN_PROGRAM_ID, &data, &metas, &account_infos, &[]) + .map_err(|_| LightSdkTypesError::CpiFailed) + } + + /// Invoke CPI with signer seeds (when caller needs to sign for another account). + pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), LightSdkTypesError> { + let (data, metas, account_infos) = self.build_instruction_inner()?; + AI::invoke_cpi( + &LIGHT_TOKEN_PROGRAM_ID, + &data, + &metas, + &account_infos, + signer_seeds, + ) + .map_err(|_| LightSdkTypesError::CpiFailed) + } + + /// Build instruction data, account metas, and account infos. + #[allow(clippy::type_complexity)] + fn build_instruction_inner( + &self, + ) -> Result<(Vec, Vec, Vec), LightSdkTypesError> { + let instruction_data = CreateAssociatedTokenAccountInstructionData { + bump: self.bump, + compressible_config: Some(CompressibleExtensionInstructionData { + token_account_version: DEFAULT_TOKEN_ACCOUNT_VERSION, + rent_payment: DEFAULT_PRE_PAY_NUM_EPOCHS, + compression_only: 1, // ATAs are always compression_only + write_top_up: DEFAULT_LAMPORTS_PER_WRITE, + compress_to_account_pubkey: None, + }), + }; + + let discriminator = if self.idempotent { + CREATE_ATA_IDEMPOTENT_DISCRIMINATOR + } else { + CREATE_ATA_DISCRIMINATOR + }; + + let mut data = Vec::new(); + data.push(discriminator); + instruction_data + .serialize(&mut data) + .map_err(|_| LightSdkTypesError::Borsh)?; + + // Account order matches the cToken program: + // [0] owner (readonly) + // [1] mint (readonly) + // [2] payer (signer, writable) + // [3] associated_token_account (writable) + // [4] system_program (readonly) + // [5] compressible_config (readonly) + // [6] rent_sponsor (writable) + let metas = vec![ + CpiMeta { + pubkey: self.owner.key(), + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: self.mint.key(), + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: self.payer.key(), + is_signer: true, + is_writable: true, + }, + CpiMeta { + pubkey: self.ata.key(), + is_signer: false, + is_writable: true, + }, + CpiMeta { + pubkey: self.system_program.key(), + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: self.config.key(), + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: self.sponsor.key(), + is_signer: false, + is_writable: true, + }, + ]; + + let account_infos = vec![ + self.owner.clone(), + self.mint.clone(), + self.payer.clone(), + self.ata.clone(), + self.system_program.clone(), + self.config.clone(), + self.sponsor.clone(), + ]; + + Ok((data, metas, account_infos)) + } +} diff --git a/sdk-libs/sdk-types/src/interface/cpi/impls.rs b/sdk-libs/sdk-types/src/interface/cpi/impls.rs new file mode 100644 index 0000000000..a76e746219 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/cpi/impls.rs @@ -0,0 +1,88 @@ +//! LightCpi trait implementations for v2 instruction data types. + +use light_compressed_account::{ + instruction_data::{ + compressed_proof::ValidityProof, + with_account_info::InstructionDataInvokeCpiWithAccountInfo, + with_readonly::InstructionDataInvokeCpiWithReadOnly, + }, + CpiSigner, +}; + +use super::instruction::LightCpi; + +impl LightCpi for InstructionDataInvokeCpiWithReadOnly { + fn new_cpi(cpi_signer: CpiSigner, proof: ValidityProof) -> Self { + Self { + bump: cpi_signer.bump, + invoking_program_id: cpi_signer.program_id.into(), + proof: proof.into(), + mode: 1, + ..Default::default() + } + } + fn write_to_cpi_context_first(self) -> Self { + self.write_to_cpi_context_first() + } + fn write_to_cpi_context_set(self) -> Self { + self.write_to_cpi_context_set() + } + fn execute_with_cpi_context(self) -> Self { + self.execute_with_cpi_context() + } + fn get_mode(&self) -> u8 { + self.mode + } + fn get_with_cpi_context(&self) -> bool { + self.with_cpi_context + } + fn get_cpi_context( + &self, + ) -> &light_compressed_account::instruction_data::cpi_context::CompressedCpiContext { + &self.cpi_context + } + fn get_bump(&self) -> u8 { + self.bump + } + fn has_read_only_accounts(&self) -> bool { + !self.read_only_accounts.is_empty() + } +} + +impl LightCpi for InstructionDataInvokeCpiWithAccountInfo { + fn new_cpi(cpi_signer: CpiSigner, proof: ValidityProof) -> Self { + Self { + bump: cpi_signer.bump, + invoking_program_id: cpi_signer.program_id.into(), + proof: proof.into(), + mode: 1, + ..Default::default() + } + } + fn write_to_cpi_context_first(self) -> Self { + self.write_to_cpi_context_first() + } + fn write_to_cpi_context_set(self) -> Self { + self.write_to_cpi_context_set() + } + fn execute_with_cpi_context(self) -> Self { + self.execute_with_cpi_context() + } + fn get_mode(&self) -> u8 { + self.mode + } + fn get_with_cpi_context(&self) -> bool { + self.with_cpi_context + } + fn get_cpi_context( + &self, + ) -> &light_compressed_account::instruction_data::cpi_context::CompressedCpiContext { + &self.cpi_context + } + fn get_bump(&self) -> u8 { + self.bump + } + fn has_read_only_accounts(&self) -> bool { + !self.read_only_accounts.is_empty() + } +} diff --git a/sdk-libs/sdk-types/src/interface/cpi/instruction.rs b/sdk-libs/sdk-types/src/interface/cpi/instruction.rs new file mode 100644 index 0000000000..7b1d9d28e8 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/cpi/instruction.rs @@ -0,0 +1,64 @@ +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; + +/// Base trait for Light CPI instruction types. +/// +/// This is the framework-agnostic version that provides CPI builder methods +/// without referencing SDK-specific types like `LightAccount`. +/// +/// Each SDK (`light-sdk`, `light-sdk-pinocchio`) defines its own +/// `LightCpiInstruction` trait that includes `with_light_account`. +pub trait LightCpi: Sized { + /// Creates a new CPI instruction builder with a validity proof. + /// + /// # Arguments + /// * `cpi_signer` - The CPI signer containing program ID and bump seed + /// * `proof` - Validity proof for compressed account operations + fn new_cpi(cpi_signer: crate::CpiSigner, proof: ValidityProof) -> Self; + + /// Returns the instruction mode (0 for v1, 1 for v2). + fn get_mode(&self) -> u8; + + /// Returns the CPI signer bump seed. + fn get_bump(&self) -> u8; + + /// Writes instruction to CPI context as the first operation in a batch. + /// + /// # Availability + /// Only available with the `cpi-context` feature enabled. + #[must_use = "write_to_cpi_context_first returns a new value"] + fn write_to_cpi_context_first(self) -> Self; + + /// Writes instruction to CPI context as a subsequent operation in a batch. + /// + /// # Availability + /// Only available with the `cpi-context` feature enabled. + #[must_use = "write_to_cpi_context_set returns a new value"] + fn write_to_cpi_context_set(self) -> Self; + + /// Executes all operations accumulated in CPI context. + /// + /// # Availability + /// Only available with the `cpi-context` feature enabled. + #[must_use = "execute_with_cpi_context returns a new value"] + fn execute_with_cpi_context(self) -> Self; + + /// Returns whether this instruction uses CPI context. + /// + /// # Availability + /// Only available with the `cpi-context` feature enabled. + fn get_with_cpi_context(&self) -> bool; + + /// Returns the CPI context configuration. + /// + /// # Availability + /// Only available with the `cpi-context` feature enabled. + fn get_cpi_context( + &self, + ) -> &light_compressed_account::instruction_data::cpi_context::CompressedCpiContext; + + /// Returns whether this instruction has any read-only accounts. + /// + /// # Availability + /// Only available with the `cpi-context` feature enabled. + fn has_read_only_accounts(&self) -> bool; +} diff --git a/sdk-libs/sdk-types/src/interface/cpi/invoke.rs b/sdk-libs/sdk-types/src/interface/cpi/invoke.rs new file mode 100644 index 0000000000..e1b7bdac16 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/cpi/invoke.rs @@ -0,0 +1,198 @@ +//! Generic Light system program invocation. + +use alloc::vec; + +use light_account_checks::{AccountInfoTrait, CpiMeta}; +pub use light_compressed_account::LightInstructionData; + +use crate::{ + constants::{CPI_AUTHORITY_PDA_SEED, LIGHT_SYSTEM_PROGRAM_ID}, + error::LightSdkTypesError, + interface::cpi::{account::CpiAccountsTrait, instruction::LightCpi}, +}; + +/// Trait for invoking the Light system program via CPI. +/// +/// Provides `invoke`, `invoke_write_to_cpi_context_first`, +/// `invoke_write_to_cpi_context_set`, and `invoke_execute_cpi_context` methods. +/// +/// Blanket-implemented for all types implementing `LightInstructionData + LightCpi`. +pub trait InvokeLightSystemProgram { + fn invoke( + self, + accounts: impl CpiAccountsTrait, + ) -> Result<(), LightSdkTypesError>; + fn invoke_write_to_cpi_context_first( + self, + accounts: impl CpiAccountsTrait, + ) -> Result<(), LightSdkTypesError>; + fn invoke_write_to_cpi_context_set( + self, + accounts: impl CpiAccountsTrait, + ) -> Result<(), LightSdkTypesError>; + fn invoke_execute_cpi_context( + self, + accounts: impl CpiAccountsTrait, + ) -> Result<(), LightSdkTypesError>; +} + +impl InvokeLightSystemProgram for T +where + T: LightInstructionData + LightCpi, +{ + fn invoke( + self, + accounts: impl CpiAccountsTrait, + ) -> Result<(), LightSdkTypesError> { + // Check if CPI context operations are being attempted + { + use light_compressed_account::instruction_data::cpi_context::CompressedCpiContext; + if self.get_with_cpi_context() + || *self.get_cpi_context() == CompressedCpiContext::set() + || *self.get_cpi_context() == CompressedCpiContext::first() + { + return Err(LightSdkTypesError::InvalidInstructionData); + } + } + + // Validate mode consistency + if let Some(account_mode) = accounts.get_mode() { + if account_mode != self.get_mode() { + return Err(LightSdkTypesError::InvalidInstructionData); + } + } + + let data = self.data().map_err(LightSdkTypesError::from)?; + let account_infos = accounts.to_account_infos(); + let account_metas = accounts.to_account_metas()?; + + invoke_light_system_program::(&account_infos, &account_metas, &data, self.get_bump()) + } + + fn invoke_write_to_cpi_context_first( + self, + accounts: impl CpiAccountsTrait, + ) -> Result<(), LightSdkTypesError> { + let instruction_data = self.write_to_cpi_context_first(); + inner_invoke_write_to_cpi_context_typed(instruction_data, accounts) + } + + fn invoke_write_to_cpi_context_set( + self, + accounts: impl CpiAccountsTrait, + ) -> Result<(), LightSdkTypesError> { + let instruction_data = self.write_to_cpi_context_set(); + inner_invoke_write_to_cpi_context_typed(instruction_data, accounts) + } + + fn invoke_execute_cpi_context( + self, + accounts: impl CpiAccountsTrait, + ) -> Result<(), LightSdkTypesError> { + let instruction_data = self.execute_with_cpi_context(); + + let data = instruction_data.data().map_err(LightSdkTypesError::from)?; + let account_infos = accounts.to_account_infos(); + let account_metas = accounts.to_account_metas()?; + + invoke_light_system_program::( + &account_infos, + &account_metas, + &data, + instruction_data.get_bump(), + ) + } +} + +/// Inner helper for write_to_cpi_context operations. +fn inner_invoke_write_to_cpi_context_typed( + instruction_data: T, + accounts: impl CpiAccountsTrait, +) -> Result<(), LightSdkTypesError> +where + AI: AccountInfoTrait + Clone, + T: LightInstructionData + LightCpi, +{ + if instruction_data.has_read_only_accounts() { + return Err(LightSdkTypesError::ReadOnlyAccountsNotSupportedInCpiContext); + } + + let data = instruction_data.data().map_err(LightSdkTypesError::from)?; + let account_infos = accounts.to_account_infos(); + + if account_infos.len() < 3 { + return Err(LightSdkTypesError::NotEnoughAccountKeys); + } + + let account_metas = vec![ + CpiMeta { + pubkey: account_infos[0].key(), + is_signer: true, + is_writable: true, + }, + CpiMeta { + pubkey: account_infos[1].key(), + is_signer: true, + is_writable: false, + }, + CpiMeta { + pubkey: account_infos[2].key(), + is_signer: false, + is_writable: true, + }, + ]; + + invoke_light_system_program::( + &account_infos, + &account_metas, + &data, + instruction_data.get_bump(), + ) +} + +/// Low-level function to invoke the Light system program with a PDA signer. +/// +/// Uses `AI::invoke_cpi()` to be generic over the runtime backend. +#[inline(always)] +pub fn invoke_light_system_program( + account_infos: &[AI], + account_metas: &[CpiMeta], + data: &[u8], + bump: u8, +) -> Result<(), LightSdkTypesError> { + let signer_seeds: &[&[u8]] = &[CPI_AUTHORITY_PDA_SEED, &[bump]]; + AI::invoke_cpi( + &LIGHT_SYSTEM_PROGRAM_ID, + data, + account_metas, + account_infos, + &[signer_seeds], + ) + .map_err(|_| LightSdkTypesError::CpiFailed) +} + +/// Write compressed PDA data to CPI context for chaining with subsequent operations. +/// +/// Use this when PDAs need to be written to CPI context first, which will be +/// consumed by subsequent operations (e.g., mint CPIs). +/// +/// Generic over `AccountInfoTrait` to work with both solana and pinocchio backends. +#[cfg(feature = "cpi-context")] +pub fn invoke_write_pdas_to_cpi_context( + cpi_signer: crate::CpiSigner, + proof: light_compressed_account::instruction_data::compressed_proof::ValidityProof, + new_addresses: &[light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked], + compressed_infos: &[light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo], + cpi_accounts: &crate::cpi_accounts::v2::CpiAccounts<'_, AI>, +) -> Result<(), LightSdkTypesError> { + use light_compressed_account::instruction_data::with_account_info::InstructionDataInvokeCpiWithAccountInfo; + + let cpi_context_accounts = + crate::cpi_context_write::CpiContextWriteAccounts::try_from(cpi_accounts)?; + + let instruction_data = InstructionDataInvokeCpiWithAccountInfo::new_cpi(cpi_signer, proof) + .with_new_addresses(new_addresses) + .with_account_infos(compressed_infos); + + instruction_data.invoke_write_to_cpi_context_first(cpi_context_accounts) +} diff --git a/sdk-libs/sdk-types/src/interface/cpi/mod.rs b/sdk-libs/sdk-types/src/interface/cpi/mod.rs new file mode 100644 index 0000000000..6b17390817 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/cpi/mod.rs @@ -0,0 +1,22 @@ +//! Generic CPI module for Light system program invocation. +//! +//! Uses v2 `CpiAccounts<'a, T: AccountInfoTrait>` from light-sdk-types. +//! All CPI calls go through `AI::invoke_cpi()` for framework independence. + +pub mod account; +#[cfg(feature = "token")] +pub mod create_mints; +#[cfg(feature = "token")] +pub mod create_token_accounts; +pub mod impls; +mod instruction; +pub mod invoke; + +pub use account::CpiAccountsTrait; +pub use instruction::LightCpi; +#[cfg(feature = "cpi-context")] +pub use invoke::invoke_write_pdas_to_cpi_context; +pub use invoke::{invoke_light_system_program, InvokeLightSystemProgram}; +pub use light_compressed_account::instruction_data::traits::LightInstructionData; + +pub use crate::{cpi_accounts::CpiAccountsConfig, CpiSigner}; diff --git a/sdk-libs/sdk-types/src/interface/create_accounts_proof.rs b/sdk-libs/sdk-types/src/interface/create_accounts_proof.rs new file mode 100644 index 0000000000..be35fadaf8 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/create_accounts_proof.rs @@ -0,0 +1,23 @@ +use light_compressed_account::instruction_data::{ + compressed_proof::ValidityProof, data::PackedAddressTreeInfo, +}; + +use crate::{AnchorDeserialize, AnchorSerialize}; + +/// 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, + /// State merkle tree index (needed for mint creation decompress validation). + /// This is optional to maintain backwards compatibility. + pub state_tree_index: Option, + /// Offset in remaining_accounts where Light system accounts start. + pub system_accounts_offset: u8, +} diff --git a/sdk-libs/sdk-types/src/interface/mod.rs b/sdk-libs/sdk-types/src/interface/mod.rs new file mode 100644 index 0000000000..16e2982e52 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/mod.rs @@ -0,0 +1,78 @@ +//! Framework-agnostic interface for Light Protocol compressible accounts. + +pub mod account; +pub mod accounts; +pub mod create_accounts_proof; +pub mod program; + +// LightCpi trait + CPI builder (no runtime dep) +pub mod cpi; + +// --- Re-exports from light-compressible --- +// ============================================================================= +// FLAT RE-EXPORTS +// ============================================================================= + +// --- account/ --- +#[cfg(all(not(target_os = "solana"), feature = "std"))] +pub use account::pack::Pack; +// --- program/ --- +#[cfg(feature = "token")] +pub use account::token_seeds::{PackedTokenData, TokenDataWithPackedSeeds, TokenDataWithSeeds}; +pub use account::{ + compression_info::{ + claim_completed_epoch_rent, CompressAs, CompressedAccountData, CompressedInitSpace, + CompressionInfo, CompressionInfoField, CompressionState, HasCompressionInfo, Space, + COMPRESSION_INFO_SIZE, OPTION_COMPRESSION_INFO_SPACE, + }, + light_account::{AccountType, LightAccount}, + pack::Unpack, + pda_seeds::{HasTokenVariant, PdaSeedDerivation}, +}; +// --- accounts/ --- +pub use accounts::{ + finalize::{LightFinalize, LightPreInit}, + init_compressed_account::{prepare_compressed_account_on_init, reimburse_rent}, +}; +// --- cpi/ --- +pub use cpi::{ + account::CpiAccountsTrait, + invoke::{invoke_light_system_program, InvokeLightSystemProgram}, + LightCpi, +}; +// --- Re-exports --- +pub use create_accounts_proof::CreateAccountsProof; +pub use light_compressible::rent; +#[cfg(feature = "token")] +pub use program::decompression::processor::process_decompress_accounts_idempotent; +#[cfg(feature = "token")] +pub use program::decompression::token::prepare_token_account_for_decompression; +#[cfg(feature = "token")] +pub use program::variant::{PackedTokenSeeds, UnpackedTokenSeeds}; +pub use program::{ + compression::{ + pda::prepare_account_for_compression, + processor::{ + process_compress_pda_accounts_idempotent, CompressAndCloseParams, CompressCtx, + CompressDispatchFn, + }, + }, + config::{ + process_initialize_light_config_checked, process_update_light_config, + InitializeLightConfigParams, LightConfig, UpdateLightConfigParams, LIGHT_CONFIG_SEED, + MAX_ADDRESS_TREES_PER_SPACE, + }, + decompression::{ + pda::prepare_account_for_decompression, + processor::{ + process_decompress_pda_accounts_idempotent, DecompressCtx, DecompressIdempotentParams, + DecompressVariant, + }, + }, + validation::{ + extract_tail_accounts, is_pda_initialized, should_skip_compression, + split_at_system_accounts_offset, validate_compress_accounts, validate_decompress_accounts, + ValidatedPdaContext, + }, + variant::{IntoVariant, LightAccountVariantTrait, PackedLightAccountVariantTrait}, +}; diff --git a/sdk-libs/sdk-types/src/interface/program/compression/mod.rs b/sdk-libs/sdk-types/src/interface/program/compression/mod.rs new file mode 100644 index 0000000000..9dd7eee930 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/program/compression/mod.rs @@ -0,0 +1,4 @@ +//! Compression functions for PDA accounts. + +pub mod pda; +pub mod processor; diff --git a/sdk-libs/sdk/src/interface/program/compression/pda.rs b/sdk-libs/sdk-types/src/interface/program/compression/pda.rs similarity index 73% rename from sdk-libs/sdk/src/interface/program/compression/pda.rs rename to sdk-libs/sdk-types/src/interface/program/compression/pda.rs index 9ec9f40327..c5ab44fce1 100644 --- a/sdk-libs/sdk/src/interface/program/compression/pda.rs +++ b/sdk-libs/sdk-types/src/interface/program/compression/pda.rs @@ -3,10 +3,7 @@ //! These functions are generic over account types and can be reused by the macro. //! The compress flow uses a dispatch callback pattern (same as decompress). -use anchor_lang::{ - prelude::*, - solana_program::{clock::Clock, rent::Rent, sysvar::Sysvar}, -}; +use light_account_checks::AccountInfoTrait; use light_compressed_account::{ address::derive_address, compressed_account::PackedMerkleContext, @@ -14,12 +11,15 @@ use light_compressed_account::{ }; use light_compressible::{rent::AccountRentState, DECOMPRESSED_PDA_DISCRIMINATOR}; use light_hasher::{sha256::Sha256BE, Hasher, Sha256}; -use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; -use solana_program_error::ProgramError; use crate::{ - instruction::account_meta::{CompressedAccountMeta, CompressedAccountMetaTrait}, - interface::{program::compression::processor::CompressCtx, LightAccount}, + error::LightSdkTypesError, + instruction::account_meta::{ + CompressedAccountMeta, CompressedAccountMetaNoLamportsNoAddress, CompressedAccountMetaTrait, + }, + interface::{ + account::compression_info::HasCompressionInfo, program::compression::processor::CompressCtx, + }, LightDiscriminator, }; @@ -38,21 +38,23 @@ use crate::{ /// * `compressed_account_meta` - Compressed account metadata /// * `pda_index` - Index of the PDA in the accounts array (for tracking closes) /// * `ctx` - Mutable context ref - pushes results here -pub fn prepare_account_for_compression<'info, A>( - account_info: &AccountInfo<'info>, +pub fn prepare_account_for_compression( + account_info: &AI, account_data: &mut A, compressed_account_meta: &CompressedAccountMetaNoLamportsNoAddress, pda_index: usize, - ctx: &mut CompressCtx<'_, 'info>, -) -> std::result::Result<(), ProgramError> + ctx: &mut CompressCtx<'_, AI>, +) -> Result<(), LightSdkTypesError> where - A: LightAccount + LightDiscriminator + Clone + AnchorSerialize, + AI: AccountInfoTrait, + A: HasCompressionInfo + LightDiscriminator + Clone + borsh::BorshSerialize, { // v2 address derive using PDA as seed + let account_key = account_info.key(); let derived_c_pda = derive_address( - &account_info.key.to_bytes(), - &ctx.light_config.address_space[0].to_bytes(), - &ctx.program_id.to_bytes(), + &account_key, + &ctx.light_config.address_space[0], + ctx.program_id, ); let meta_with_address = CompressedAccountMeta { @@ -61,14 +63,13 @@ where output_state_tree_index: compressed_account_meta.output_state_tree_index, }; - let current_slot = Clock::get()?.slot; + let current_slot = AI::get_current_slot().map_err(LightSdkTypesError::AccountError)?; let bytes = account_info.data_len() as u64; let current_lamports = account_info.lamports(); - let rent_exemption_lamports = Rent::get() - .map_err(|_| ProgramError::Custom(0))? - .minimum_balance(bytes as usize); + let rent_exemption_lamports = + AI::get_min_rent_balance(bytes as usize).map_err(LightSdkTypesError::AccountError)?; - let ci = account_data.compression_info(); + let ci = account_data.compression_info()?; let last_claimed_slot = ci.last_claimed_slot(); let rent_cfg = ci.rent_config; @@ -84,44 +85,41 @@ where .is_compressible(&rent_cfg, rent_exemption_lamports) .is_none() { - solana_msg::msg!("pda not yet compressible, skipping batch"); ctx.has_non_compressible = true; return Ok(()); } - // Mark as compressed using LightAccount trait - account_data.compression_info_mut().set_compressed(); + // Mark as compressed + account_data.compression_info_mut()?.set_compressed(); // Serialize updated account data back (includes 8-byte discriminator) { let mut data = account_info .try_borrow_mut_data() - .map_err(|_| ProgramError::Custom(2))?; + .map_err(LightSdkTypesError::AccountError)?; // Write discriminator first data[..8].copy_from_slice(&A::LIGHT_DISCRIMINATOR); // Write serialized account data after discriminator let writer = &mut &mut data[8..]; account_data .serialize(writer) - .map_err(|_| ProgramError::Custom(3))?; + .map_err(|_| LightSdkTypesError::Borsh)?; } // Create compressed account with canonical compressed CompressionInfo for hashing let mut compressed_data = account_data.clone(); - *compressed_data.compression_info_mut() = crate::compressible::CompressionInfo::compressed(); + *compressed_data.compression_info_mut()? = + crate::interface::account::compression_info::CompressionInfo::compressed(); // Hash the data (discriminator NOT included per protocol convention) - let data_bytes = compressed_data - .try_to_vec() - .map_err(|_| ProgramError::Custom(4))?; - let mut output_data_hash = Sha256::hash(&data_bytes).map_err(|_| ProgramError::Custom(5))?; + let data_bytes = borsh::to_vec(&compressed_data).map_err(|_| LightSdkTypesError::Borsh)?; + let mut output_data_hash = Sha256::hash(&data_bytes).map_err(LightSdkTypesError::Hasher)?; output_data_hash[0] = 0; // Zero first byte per protocol convention // Build input account info (placeholder compressed account from init) // The init created a placeholder with DECOMPRESSED_PDA_DISCRIMINATOR and PDA pubkey as data let tree_info = compressed_account_meta.tree_info; - let input_data_hash = - Sha256BE::hash(&account_info.key.to_bytes()).map_err(|_| ProgramError::Custom(6))?; + let input_data_hash = Sha256BE::hash(&account_key).map_err(LightSdkTypesError::Hasher)?; let input_account_info = InAccountInfo { data_hash: input_data_hash, lamports: 0, diff --git a/sdk-libs/sdk-types/src/interface/program/compression/processor.rs b/sdk-libs/sdk-types/src/interface/program/compression/processor.rs new file mode 100644 index 0000000000..20018c1de3 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/program/compression/processor.rs @@ -0,0 +1,151 @@ +//! Compression instruction processor. + +use alloc::vec::Vec; + +use light_account_checks::AccountInfoTrait; +use light_compressed_account::instruction_data::{ + compressed_proof::ValidityProof, + with_account_info::{CompressedAccountInfo, InstructionDataInvokeCpiWithAccountInfo}, +}; + +use crate::{ + cpi_accounts::v2::CpiAccounts, + error::LightSdkTypesError, + instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + interface::{cpi::InvokeLightSystemProgram, program::config::LightConfig}, + AnchorDeserialize, AnchorSerialize, CpiSigner, +}; + +/// Account indices within remaining_accounts for compress instructions. +const FEE_PAYER_INDEX: usize = 0; +const CONFIG_INDEX: usize = 1; +const RENT_SPONSOR_INDEX: usize = 2; + +/// Parameters for compress_and_close instruction. +/// Matches SDK's SaveAccountsData field order for compatibility. +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CompressAndCloseParams { + /// Validity proof for compressed account verification + pub proof: ValidityProof, + /// Accounts to compress (meta only - data read from PDA) + pub compressed_accounts: Vec, + /// Offset into remaining_accounts where Light system accounts begin + pub system_accounts_offset: u8, +} + +/// Context struct holding all data needed for compression. +/// Generic over AccountInfoTrait to work with both solana and pinocchio. +pub struct CompressCtx<'a, AI: AccountInfoTrait> { + pub program_id: &'a [u8; 32], + pub remaining_accounts: &'a [AI], + pub rent_sponsor: &'a AI, + pub light_config: &'a LightConfig, + /// Internal vec - dispatch functions push results here + pub compressed_account_infos: Vec, + /// Track which PDA indices to close + pub pda_indices_to_close: Vec, + /// Set to true if any account is not yet compressible. + /// When set, the entire batch is skipped (no CPI, no closes). + pub has_non_compressible: bool, +} + +/// Callback type for discriminator-based dispatch. +/// MACRO-GENERATED: Just a match statement routing to prepare_account_for_compression. +pub type CompressDispatchFn = fn( + account_info: &AI, + compressed_account_meta: &CompressedAccountMetaNoLamportsNoAddress, + index: usize, + ctx: &mut CompressCtx<'_, AI>, +) -> Result<(), LightSdkTypesError>; + +/// Process compress-and-close for PDA accounts (idempotent). +/// +/// Iterates over PDA accounts, dispatches each for compression via `dispatch_fn`, +/// then invokes the Light system program CPI to commit compressed state, +/// and closes the PDA accounts (transferring lamports to rent_sponsor). +/// +/// Idempotent: if any account is not yet compressible (rent function check fails), +/// the entire batch is silently skipped. +#[inline(never)] +pub fn process_compress_pda_accounts_idempotent( + remaining_accounts: &[AI], + params: &CompressAndCloseParams, + dispatch_fn: CompressDispatchFn, + cpi_signer: CpiSigner, + program_id: &[u8; 32], +) -> Result<(), LightSdkTypesError> { + let system_accounts_offset = params.system_accounts_offset as usize; + let num_pdas = params.compressed_accounts.len(); + + if num_pdas == 0 { + return Err(LightSdkTypesError::InvalidInstructionData); + } + + // 2. Load and validate config + let config = LightConfig::load_checked(&remaining_accounts[CONFIG_INDEX], program_id)?; + + // 3. Validate rent_sponsor + let rent_sponsor = &remaining_accounts[RENT_SPONSOR_INDEX]; + config.validate_rent_sponsor_account::(rent_sponsor)?; + + // 4. PDA accounts are at the tail of remaining_accounts + let pda_start = remaining_accounts + .len() + .checked_sub(num_pdas) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + + // 5. Run dispatch for each PDA + let (compressed_account_infos, pda_indices_to_close, has_non_compressible) = { + let mut ctx = CompressCtx { + program_id, + remaining_accounts, + rent_sponsor, + light_config: &config, + compressed_account_infos: Vec::with_capacity(num_pdas), + pda_indices_to_close: Vec::with_capacity(num_pdas), + has_non_compressible: false, + }; + + for (i, meta) in params.compressed_accounts.iter().enumerate() { + let pda_index = pda_start + i; + dispatch_fn(&remaining_accounts[pda_index], meta, pda_index, &mut ctx)?; + } + + ( + ctx.compressed_account_infos, + ctx.pda_indices_to_close, + ctx.has_non_compressible, + ) + }; + + // 6. Idempotent: if any account is not yet compressible, skip entire batch + if has_non_compressible { + return Ok(()); + } + + // 7. Build CPI instruction data + let mut cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo::new( + program_id.into(), + cpi_signer.bump, + params.proof.into(), + ); + cpi_ix_data.account_infos = compressed_account_infos; + + // 8. Build CpiAccounts from system accounts slice (excluding PDA accounts at tail) + let cpi_accounts = CpiAccounts::new( + &remaining_accounts[FEE_PAYER_INDEX], + &remaining_accounts[system_accounts_offset..pda_start], + cpi_signer, + ); + + // 9. Invoke Light system program CPI + cpi_ix_data.invoke::(cpi_accounts)?; + + // 10. Close PDA accounts, transferring lamports to rent_sponsor + for pda_index in &pda_indices_to_close { + light_account_checks::close_account(&remaining_accounts[*pda_index], rent_sponsor) + .map_err(LightSdkTypesError::AccountError)?; + } + + Ok(()) +} diff --git a/sdk-libs/sdk-types/src/interface/program/config/create.rs b/sdk-libs/sdk-types/src/interface/program/config/create.rs new file mode 100644 index 0000000000..bdf347e8fe --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/program/config/create.rs @@ -0,0 +1,263 @@ +//! Config initialization instructions (generic over AccountInfoTrait). + +use alloc::vec::Vec; + +use light_account_checks::{ + checks::check_signer, + discriminator::{Discriminator, DISCRIMINATOR_LEN}, + AccountInfoTrait, +}; +use light_compressible::rent::RentConfig; + +use super::{state::LightConfig, validate_address_space_no_duplicates, LIGHT_CONFIG_SEED}; +use crate::{error::LightSdkTypesError, AnchorSerialize}; + +/// BPFLoaderUpgradeab1e11111111111111111111111 as raw bytes. +const BPF_LOADER_UPGRADEABLE_ID: [u8; 32] = [ + 2, 168, 246, 145, 78, 136, 161, 110, 57, 90, 225, 40, 148, 143, 144, 16, 207, 227, 47, 228, + 248, 212, 16, 185, 221, 165, 30, 160, 42, 103, 43, 122, +]; + +/// UpgradeableLoaderState::ProgramData layout (manual parsing, no bincode dep): +/// - bytes 0..4: variant tag (u32 LE, must be 3 for ProgramData) +/// - bytes 4..12: slot (u64 LE) +/// - byte 12: Option discriminant (0=None, 1=Some) +/// - bytes 13..45: authority pubkey (32 bytes, only valid when discriminant=1) +const PROGRAM_DATA_VARIANT_TAG: u32 = 3; +const PROGRAM_DATA_MIN_LEN: usize = 45; + +/// Creates a new compressible config PDA. +/// +/// # Required Validation (must be done by caller) +/// The caller MUST validate that the signer is the program's upgrade authority. +/// Use `process_initialize_light_config_checked` for the version that does this. +#[allow(clippy::too_many_arguments)] +pub fn process_initialize_light_config( + config_account: &AI, + update_authority: &AI, + rent_sponsor: &[u8; 32], + compression_authority: &[u8; 32], + rent_config: RentConfig, + write_top_up: u32, + address_space: Vec<[u8; 32]>, + config_bump: u8, + payer: &AI, + system_program: &AI, + program_id: &[u8; 32], +) -> Result<(), LightSdkTypesError> { + // CHECK: config_bump must be 0 + if config_bump != 0 { + return Err(LightSdkTypesError::ConstraintViolation); + } + + // CHECK: not already initialized + if !config_account.data_is_empty() { + return Err(LightSdkTypesError::ConstraintViolation); + } + + // CHECK: exactly 1 address space + if address_space.len() != 1 { + return Err(LightSdkTypesError::ConstraintViolation); + } + + // CHECK: unique pubkeys in address_space + validate_address_space_no_duplicates(&address_space)?; + + // CHECK: signer + check_signer(update_authority).map_err(LightSdkTypesError::AccountError)?; + + // CHECK: PDA derivation + let (derived_pda, bump) = LightConfig::derive_pda_bytes::(program_id, config_bump); + if derived_pda != config_account.key() { + return Err(LightSdkTypesError::ConstraintViolation); + } + + // Derive rent_sponsor_bump for storage + let (derived_rent_sponsor, rent_sponsor_bump) = + LightConfig::derive_rent_sponsor_pda_bytes::(program_id); + if *rent_sponsor != derived_rent_sponsor { + return Err(LightSdkTypesError::InvalidRentSponsor); + } + + let account_size = LightConfig::size_for_address_space(address_space.len()); + let rent_lamports = + AI::get_min_rent_balance(account_size).map_err(LightSdkTypesError::AccountError)?; + + // Create PDA using AccountInfoTrait + let config_bump_bytes = (config_bump as u16).to_le_bytes(); + let seeds: &[&[u8]] = &[LIGHT_CONFIG_SEED, config_bump_bytes.as_ref(), &[bump]]; + + config_account.create_pda_account( + rent_lamports, + account_size as u64, + program_id, + seeds, + payer, + &[], + system_program, + )?; + + let config = LightConfig { + version: 1, + write_top_up, + update_authority: update_authority.key(), + rent_sponsor: *rent_sponsor, + compression_authority: *compression_authority, + rent_config, + config_bump, + bump, + rent_sponsor_bump, + address_space, + }; + + let mut data = config_account + .try_borrow_mut_data() + .map_err(LightSdkTypesError::AccountError)?; + + // Write discriminator first + data[..DISCRIMINATOR_LEN].copy_from_slice(&LightConfig::LIGHT_DISCRIMINATOR); + + // Serialize config data after discriminator + config + .serialize(&mut &mut data[DISCRIMINATOR_LEN..]) + .map_err(|_| LightSdkTypesError::Borsh)?; + + Ok(()) +} + +/// Checks that the signer is the program's upgrade authority. +/// +/// Manually parses the UpgradeableLoaderState::ProgramData layout (45 bytes) +/// to avoid a bincode dependency. +pub fn check_program_upgrade_authority( + program_id: &[u8; 32], + program_data_account: &AI, + authority: &AI, +) -> Result<(), LightSdkTypesError> { + // CHECK: program data PDA + let (expected_program_data, _) = + AI::find_program_address(&[program_id], &BPF_LOADER_UPGRADEABLE_ID); + if program_data_account.key() != expected_program_data { + return Err(LightSdkTypesError::ConstraintViolation); + } + + let data = program_data_account + .try_borrow_data() + .map_err(LightSdkTypesError::AccountError)?; + + if data.len() < PROGRAM_DATA_MIN_LEN { + return Err(LightSdkTypesError::AccountDataTooSmall); + } + + // Parse variant tag (4 bytes, u32 LE) + let variant_tag = u32::from_le_bytes(data[0..4].try_into().unwrap()); + if variant_tag != PROGRAM_DATA_VARIANT_TAG { + return Err(LightSdkTypesError::ConstraintViolation); + } + + // Parse Option at offset 12 + let option_discriminant = data[12]; + let upgrade_authority: [u8; 32] = match option_discriminant { + 0 => { + // None - program has no upgrade authority + return Err(LightSdkTypesError::ConstraintViolation); + } + 1 => { + let mut auth = [0u8; 32]; + auth.copy_from_slice(&data[13..45]); + // Check for invalid zero authority + if auth == [0u8; 32] { + return Err(LightSdkTypesError::ConstraintViolation); + } + auth + } + _ => { + return Err(LightSdkTypesError::ConstraintViolation); + } + }; + + // CHECK: authority is signer + check_signer(authority).map_err(LightSdkTypesError::AccountError)?; + + // CHECK: authority matches upgrade authority + if authority.key() != upgrade_authority { + return Err(LightSdkTypesError::ConstraintViolation); + } + + Ok(()) +} + +/// Creates a new compressible config PDA with upgrade authority check. +#[allow(clippy::too_many_arguments)] +pub fn process_initialize_light_config_checked( + config_account: &AI, + update_authority: &AI, + program_data_account: &AI, + rent_sponsor: &[u8; 32], + compression_authority: &[u8; 32], + rent_config: RentConfig, + write_top_up: u32, + address_space: Vec<[u8; 32]>, + config_bump: u8, + payer: &AI, + system_program: &AI, + program_id: &[u8; 32], +) -> Result<(), LightSdkTypesError> { + check_program_upgrade_authority::(program_id, program_data_account, update_authority)?; + + process_initialize_light_config( + config_account, + update_authority, + rent_sponsor, + compression_authority, + rent_config, + write_top_up, + address_space, + config_bump, + payer, + system_program, + program_id, + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_upgradeable_loader_state_parsing() { + // Build a synthetic ProgramData account matching the manual layout + let mut data = [0u8; 45]; + + // Variant tag = 3 (ProgramData) + data[0..4].copy_from_slice(&3u32.to_le_bytes()); + + // Slot = 42 + data[4..12].copy_from_slice(&42u64.to_le_bytes()); + + // Option discriminant = 1 (Some) + data[12] = 1; + + // Authority pubkey = [1..=32] + let authority: [u8; 32] = core::array::from_fn(|i| (i + 1) as u8); + data[13..45].copy_from_slice(&authority); + + // Parse variant tag + let tag = u32::from_le_bytes(data[0..4].try_into().unwrap()); + assert_eq!(tag, PROGRAM_DATA_VARIANT_TAG); + + // Parse slot + let slot = u64::from_le_bytes(data[4..12].try_into().unwrap()); + assert_eq!(slot, 42); + + // Parse authority + assert_eq!(data[12], 1); + let mut parsed_auth = [0u8; 32]; + parsed_auth.copy_from_slice(&data[13..45]); + assert_eq!(parsed_auth, authority); + + // Test None case + data[12] = 0; + assert_eq!(data[12], 0); + } +} diff --git a/sdk-libs/sdk-types/src/interface/program/config/mod.rs b/sdk-libs/sdk-types/src/interface/program/config/mod.rs new file mode 100644 index 0000000000..17fb18f214 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/program/config/mod.rs @@ -0,0 +1,150 @@ +//! LightConfig management for compressible accounts. + +use alloc::vec::Vec; + +use light_account_checks::AccountInfoTrait; +use light_compressible::rent::RentConfig; + +use crate::{error::LightSdkTypesError, AnchorDeserialize, AnchorSerialize}; + +pub mod create; +mod state; +pub mod update; + +// --- Constants --- + +pub const LIGHT_CONFIG_SEED: &[u8] = b"compressible_config"; +pub const MAX_ADDRESS_TREES_PER_SPACE: usize = 1; + +// --- Re-exports --- +// Re-export Discriminator trait so users can access LightConfig::LIGHT_DISCRIMINATOR +pub use light_account_checks::discriminator::Discriminator; +pub use state::LightConfig; + +pub use crate::constants::RENT_SPONSOR_SEED; + +// ============================================================================= +// Instruction params (serialized by client, deserialized by program) +// ============================================================================= + +/// Parameters for initialize_compression_config instruction. +/// Uses `[u8; 32]` for pubkeys - borsh-compatible with `solana_pubkey::Pubkey`. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct InitializeLightConfigParams { + pub rent_sponsor: [u8; 32], + pub compression_authority: [u8; 32], + pub rent_config: RentConfig, + pub write_top_up: u32, + pub address_space: Vec<[u8; 32]>, + pub config_bump: u8, +} + +/// Parameters for update_compression_config instruction. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct UpdateLightConfigParams { + pub new_update_authority: Option<[u8; 32]>, + pub new_rent_sponsor: Option<[u8; 32]>, + pub new_compression_authority: Option<[u8; 32]>, + pub new_rent_config: Option, + pub new_write_top_up: Option, + pub new_address_space: Option>, +} + +// ============================================================================= +// Top-level wrapper functions (remaining_accounts + instruction_data) +// ============================================================================= + +/// Initialize a LightConfig PDA with upgrade authority check. +/// +/// Account layout in remaining_accounts: +/// - [0] payer (signer, mut) +/// - [1] config_account (mut) +/// - [2] program_data_account (readonly) +/// - [3] authority (signer) +/// - [4] system_program +pub fn process_initialize_light_config_checked( + remaining_accounts: &[AI], + instruction_data: &[u8], + program_id: &[u8; 32], +) -> Result<(), LightSdkTypesError> { + if remaining_accounts.len() < 5 { + return Err(LightSdkTypesError::NotEnoughAccountKeys); + } + + let params = InitializeLightConfigParams::try_from_slice(instruction_data) + .map_err(|_| LightSdkTypesError::Borsh)?; + + create::process_initialize_light_config_checked( + &remaining_accounts[1], // config_account + &remaining_accounts[3], // authority + &remaining_accounts[2], // program_data_account + ¶ms.rent_sponsor, + ¶ms.compression_authority, + params.rent_config, + params.write_top_up, + params.address_space, + params.config_bump, + &remaining_accounts[0], // payer + &remaining_accounts[4], // system_program + program_id, + ) +} + +/// Update an existing LightConfig PDA. +/// +/// Account layout in remaining_accounts: +/// - [0] config_account (mut) +/// - [1] authority (signer) +pub fn process_update_light_config( + remaining_accounts: &[AI], + instruction_data: &[u8], + program_id: &[u8; 32], +) -> Result<(), LightSdkTypesError> { + if remaining_accounts.len() < 2 { + return Err(LightSdkTypesError::NotEnoughAccountKeys); + } + + let params = UpdateLightConfigParams::try_from_slice(instruction_data) + .map_err(|_| LightSdkTypesError::Borsh)?; + + update::process_update_light_config( + &remaining_accounts[0], // config_account + &remaining_accounts[1], // authority + params.new_update_authority.as_ref(), + params.new_rent_sponsor.as_ref(), + params.new_compression_authority.as_ref(), + params.new_rent_config, + params.new_write_top_up, + params.new_address_space, + program_id, + ) +} + +// --- Shared validators (used by create and update) --- + +/// Validates that address_space contains no duplicate pubkeys +pub(super) fn validate_address_space_no_duplicates( + address_space: &[[u8; 32]], +) -> Result<(), LightSdkTypesError> { + use alloc::collections::BTreeSet; + let mut seen = BTreeSet::new(); + for pubkey in address_space { + if !seen.insert(pubkey) { + return Err(LightSdkTypesError::ConstraintViolation); + } + } + Ok(()) +} + +/// Validates that new_address_space only adds to existing address_space (no removals) +pub(super) fn validate_address_space_only_adds( + existing_address_space: &[[u8; 32]], + new_address_space: &[[u8; 32]], +) -> Result<(), LightSdkTypesError> { + for existing_pubkey in existing_address_space { + if !new_address_space.contains(existing_pubkey) { + return Err(LightSdkTypesError::ConstraintViolation); + } + } + Ok(()) +} diff --git a/sdk-libs/sdk-types/src/interface/program/config/state.rs b/sdk-libs/sdk-types/src/interface/program/config/state.rs new file mode 100644 index 0000000000..5a3c5c42ba --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/program/config/state.rs @@ -0,0 +1,162 @@ +//! LightConfig state struct and methods. + +use alloc::vec::Vec; + +use light_account_checks::{ + checks::check_discriminator, + discriminator::{Discriminator, DISCRIMINATOR_LEN}, + AccountInfoTrait, +}; +use light_compressible::rent::RentConfig; + +use super::{LIGHT_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE}; +use crate::{error::LightSdkTypesError, AnchorDeserialize, AnchorSerialize}; + +/// Global configuration for compressible accounts +#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)] +pub struct LightConfig { + /// Config version for future upgrades + pub version: u8, + /// Lamports to top up on each write (heuristic) + pub write_top_up: u32, + /// Authority that can update the config + pub update_authority: [u8; 32], + /// Account that receives rent from compressed PDAs + pub rent_sponsor: [u8; 32], + /// Authority that can compress/close PDAs (distinct from rent_sponsor) + pub compression_authority: [u8; 32], + /// Rent function parameters for compressibility and distribution + pub rent_config: RentConfig, + /// Config bump seed (0) + pub config_bump: u8, + /// Config PDA bump seed + pub bump: u8, + /// Rent sponsor PDA bump seed + pub rent_sponsor_bump: u8, + /// Address space for compressed accounts (currently 1 address_tree allowed) + pub address_space: Vec<[u8; 32]>, +} + +/// Implement the Light Discriminator trait for LightConfig +impl Discriminator for LightConfig { + const LIGHT_DISCRIMINATOR: [u8; 8] = *b"LightCfg"; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; +} + +impl LightConfig { + /// Total account size including discriminator + pub const LEN: usize = DISCRIMINATOR_LEN + + 1 + + 4 + + 32 + + 32 + + 32 + + core::mem::size_of::() + + 1 + + 1 + + 1 + + 4 + + (32 * MAX_ADDRESS_TREES_PER_SPACE); + + /// Calculate the exact size needed for a LightConfig with the given + /// number of address spaces (includes discriminator) + pub fn size_for_address_space(num_address_trees: usize) -> usize { + DISCRIMINATOR_LEN + + 1 + + 4 + + 32 + + 32 + + 32 + + core::mem::size_of::() + + 1 + + 1 + + 1 + + 4 + + (32 * num_address_trees) + } + + /// Derives the config PDA address (returns raw bytes). + /// Generic over AccountInfoTrait for framework-agnostic PDA derivation. + pub fn derive_pda_bytes( + program_id: &[u8; 32], + config_bump: u8, + ) -> ([u8; 32], u8) { + let config_bump_u16 = config_bump as u16; + AI::find_program_address( + &[LIGHT_CONFIG_SEED, &config_bump_u16.to_le_bytes()], + program_id, + ) + } + + /// Derives the rent sponsor PDA address (returns raw bytes). + pub fn derive_rent_sponsor_pda_bytes( + program_id: &[u8; 32], + ) -> ([u8; 32], u8) { + AI::find_program_address(&[super::RENT_SPONSOR_SEED], program_id) + } + + /// Validates rent_sponsor matches config and returns stored bump for signing. + pub fn validate_rent_sponsor_account( + &self, + rent_sponsor: &AI, + ) -> Result { + if rent_sponsor.key() != self.rent_sponsor { + return Err(LightSdkTypesError::InvalidRentSponsor); + } + Ok(self.rent_sponsor_bump) + } + + /// Checks the config account + pub fn validate(&self) -> Result<(), LightSdkTypesError> { + if self.version != 1 { + return Err(LightSdkTypesError::ConstraintViolation); + } + if self.address_space.len() != 1 { + return Err(LightSdkTypesError::ConstraintViolation); + } + // For now, only allow config_bump = 0 to keep it simple + if self.config_bump != 0 { + return Err(LightSdkTypesError::ConstraintViolation); + } + Ok(()) + } + + /// Loads and validates config from account, checking owner, discriminator, and PDA derivation. + /// Generic over AccountInfoTrait - works with both solana and pinocchio. + #[inline(never)] + pub fn load_checked( + account: &AI, + program_id: &[u8; 32], + ) -> Result { + // CHECK: Owner + if !account.is_owned_by(program_id) { + return Err(LightSdkTypesError::ConstraintViolation); + } + + let data = account + .try_borrow_data() + .map_err(|_| LightSdkTypesError::ConstraintViolation)?; + + // CHECK: Discriminator using light-account-checks + check_discriminator::(&data).map_err(|_| LightSdkTypesError::ConstraintViolation)?; + + // Deserialize from offset after discriminator + let config = Self::try_from_slice(&data[DISCRIMINATOR_LEN..]) + .map_err(|_| LightSdkTypesError::Borsh)?; + config.validate()?; + + // CHECK: PDA derivation + let (expected_pda, _) = AI::find_program_address( + &[ + LIGHT_CONFIG_SEED, + &(config.config_bump as u16).to_le_bytes(), + ], + program_id, + ); + if expected_pda != account.key() { + return Err(LightSdkTypesError::ConstraintViolation); + } + + Ok(config) + } +} diff --git a/sdk-libs/sdk-types/src/interface/program/config/update.rs b/sdk-libs/sdk-types/src/interface/program/config/update.rs new file mode 100644 index 0000000000..2d11bc62d4 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/program/config/update.rs @@ -0,0 +1,76 @@ +//! Config update instruction (generic over AccountInfoTrait). + +use alloc::vec::Vec; + +use light_account_checks::{ + checks::check_signer, discriminator::DISCRIMINATOR_LEN, AccountInfoTrait, +}; +use light_compressible::rent::RentConfig; + +use super::{ + state::LightConfig, validate_address_space_no_duplicates, validate_address_space_only_adds, + MAX_ADDRESS_TREES_PER_SPACE, +}; +use crate::{error::LightSdkTypesError, AnchorSerialize}; + +/// Updates an existing compressible config. +#[allow(clippy::too_many_arguments)] +pub fn process_update_light_config( + config_account: &AI, + authority: &AI, + new_update_authority: Option<&[u8; 32]>, + new_rent_sponsor: Option<&[u8; 32]>, + new_compression_authority: Option<&[u8; 32]>, + new_rent_config: Option, + new_write_top_up: Option, + new_address_space: Option>, + owner_program_id: &[u8; 32], +) -> Result<(), LightSdkTypesError> { + // CHECK: PDA derivation + discriminator + owner + let mut config = LightConfig::load_checked(config_account, owner_program_id)?; + + // CHECK: signer + check_signer(authority).map_err(LightSdkTypesError::AccountError)?; + + // CHECK: authority + if authority.key() != config.update_authority { + return Err(LightSdkTypesError::ConstraintViolation); + } + + if let Some(new_authority) = new_update_authority { + config.update_authority = *new_authority; + } + if let Some(new_recipient) = new_rent_sponsor { + config.rent_sponsor = *new_recipient; + } + if let Some(new_auth) = new_compression_authority { + config.compression_authority = *new_auth; + } + if let Some(new_rcfg) = new_rent_config { + config.rent_config = new_rcfg; + } + if let Some(new_top_up) = new_write_top_up { + config.write_top_up = new_top_up; + } + if let Some(new_address_space) = new_address_space { + // CHECK: address space length + if new_address_space.len() != MAX_ADDRESS_TREES_PER_SPACE { + return Err(LightSdkTypesError::ConstraintViolation); + } + + validate_address_space_no_duplicates(&new_address_space)?; + validate_address_space_only_adds(&config.address_space, &new_address_space)?; + + config.address_space = new_address_space; + } + + let mut data = config_account + .try_borrow_mut_data() + .map_err(LightSdkTypesError::AccountError)?; + // Serialize after discriminator (discriminator is preserved from init) + config + .serialize(&mut &mut data[DISCRIMINATOR_LEN..]) + .map_err(|_| LightSdkTypesError::Borsh)?; + + Ok(()) +} diff --git a/sdk-libs/sdk-types/src/interface/program/decompression/create_token_account.rs b/sdk-libs/sdk-types/src/interface/program/decompression/create_token_account.rs new file mode 100644 index 0000000000..46f11f416b --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/program/decompression/create_token_account.rs @@ -0,0 +1,193 @@ +//! ATA and token account creation helpers for decompression. +//! +//! Returns `(instruction_data, account_metas)` tuples for use with `AI::invoke_cpi()`. + +use alloc::{vec, vec::Vec}; + +use light_account_checks::CpiMeta; +use light_token_interface::{ + instructions::{ + create_associated_token_account::CreateAssociatedTokenAccountInstructionData, + create_token_account::CreateTokenAccountInstructionData, + extensions::{CompressToPubkey, CompressibleExtensionInstructionData}, + }, + LIGHT_TOKEN_PROGRAM_ID, +}; + +use crate::{error::LightSdkTypesError, AnchorSerialize}; + +/// Build instruction data and account metas for creating a compressible ATA. +/// +/// Returns `(data, account_metas, program_id)` for use with `AI::invoke_cpi()`. +/// +/// # Account order (per on-chain handler): +/// 0. owner (non-mut, non-signer) +/// 1. mint (non-mut, non-signer) +/// 2. fee_payer (signer, writable) +/// 3. associated_token_account (writable, NOT signer) +/// 4. system_program (readonly) +/// 5. compressible_config (readonly) +/// 6. rent_payer (writable) +#[allow(clippy::too_many_arguments)] +pub fn build_create_ata_instruction( + wallet_owner: &[u8; 32], + mint: &[u8; 32], + fee_payer: &[u8; 32], + ata: &[u8; 32], + bump: u8, + compressible_config: &[u8; 32], + rent_sponsor: &[u8; 32], + write_top_up: u32, +) -> Result<(Vec, Vec), LightSdkTypesError> { + let instruction_data = CreateAssociatedTokenAccountInstructionData { + bump, + compressible_config: Some(CompressibleExtensionInstructionData { + token_account_version: 3, // ShaFlat version (required) + rent_payment: 16, // 24h + compression_only: 1, // Required for ATA + write_top_up, + compress_to_account_pubkey: None, + }), + }; + + let mut data = Vec::new(); + data.push(102u8); // CreateAssociatedTokenAccountIdempotent discriminator + instruction_data + .serialize(&mut data) + .map_err(|_| LightSdkTypesError::Borsh)?; + + let accounts = vec![ + CpiMeta { + pubkey: *wallet_owner, + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: *mint, + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: *fee_payer, + is_signer: true, + is_writable: true, + }, + CpiMeta { + pubkey: *ata, + is_signer: false, + is_writable: true, + }, // NOT a signer - ATA is derived + CpiMeta { + pubkey: [0u8; 32], + is_signer: false, + is_writable: false, + }, // system_program + CpiMeta { + pubkey: *compressible_config, + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: *rent_sponsor, + is_signer: false, + is_writable: true, + }, + ]; + + Ok((data, accounts)) +} + +/// Build instruction data and account metas for creating a compressible token account. +/// +/// Returns `(data, account_metas)` for use with `AI::invoke_cpi()`. +/// +/// # Account order: +/// 0. token_account (signer, writable) +/// 1. mint (readonly) +/// 2. fee_payer (signer, writable) +/// 3. compressible_config (readonly) +/// 4. system_program (readonly) +/// 5. rent_sponsor (writable) +#[allow(clippy::too_many_arguments)] +pub fn build_create_token_account_instruction( + token_account: &[u8; 32], + mint: &[u8; 32], + owner: &[u8; 32], + fee_payer: &[u8; 32], + compressible_config: &[u8; 32], + rent_sponsor: &[u8; 32], + write_top_up: u32, + signer_seeds: &[&[u8]], + program_id: &[u8; 32], +) -> Result<(Vec, Vec), LightSdkTypesError> { + let bump = signer_seeds + .last() + .and_then(|s| s.first().copied()) + .ok_or(LightSdkTypesError::InvalidSeeds)?; + let seeds_without_bump: Vec> = signer_seeds + .iter() + .take(signer_seeds.len().saturating_sub(1)) + .map(|s| s.to_vec()) + .collect(); + + let compress_to_account_pubkey = CompressToPubkey { + bump, + program_id: *program_id, + seeds: seeds_without_bump, + }; + + let instruction_data = CreateTokenAccountInstructionData { + owner: light_compressed_account::Pubkey::from(*owner), + compressible_config: Some(CompressibleExtensionInstructionData { + token_account_version: 3, // ShaFlat version (required) + rent_payment: 16, // 24h + compression_only: 0, // Regular tokens can be transferred + write_top_up, + compress_to_account_pubkey: Some(compress_to_account_pubkey), + }), + }; + + let mut data = Vec::new(); + data.push(18u8); // InitializeAccount3 opcode + instruction_data + .serialize(&mut data) + .map_err(|_| LightSdkTypesError::Borsh)?; + + let accounts = vec![ + CpiMeta { + pubkey: *token_account, + is_signer: true, + is_writable: true, + }, + CpiMeta { + pubkey: *mint, + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: *fee_payer, + is_signer: true, + is_writable: true, + }, + CpiMeta { + pubkey: *compressible_config, + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: [0u8; 32], + is_signer: false, + is_writable: false, + }, // system_program + CpiMeta { + pubkey: *rent_sponsor, + is_signer: false, + is_writable: true, + }, + ]; + + Ok((data, accounts)) +} + +/// The Light Token Program ID for CPI calls. +pub const TOKEN_PROGRAM_ID: [u8; 32] = LIGHT_TOKEN_PROGRAM_ID; diff --git a/sdk-libs/sdk/src/interface/program/decompression/mod.rs b/sdk-libs/sdk-types/src/interface/program/decompression/mod.rs similarity index 54% rename from sdk-libs/sdk/src/interface/program/decompression/mod.rs rename to sdk-libs/sdk-types/src/interface/program/decompression/mod.rs index d6d99bfac8..95e0b35353 100644 --- a/sdk-libs/sdk/src/interface/program/decompression/mod.rs +++ b/sdk-libs/sdk-types/src/interface/program/decompression/mod.rs @@ -1,13 +1,8 @@ //! Decompression functions for PDA and token accounts. -#[cfg(feature = "anchor")] +#[cfg(feature = "token")] pub mod create_token_account; - -#[cfg(feature = "anchor")] -pub mod processor; - -#[cfg(feature = "anchor")] pub mod pda; - -#[cfg(feature = "anchor")] +pub mod processor; +#[cfg(feature = "token")] pub mod token; diff --git a/sdk-libs/sdk/src/interface/program/decompression/pda.rs b/sdk-libs/sdk-types/src/interface/program/decompression/pda.rs similarity index 65% rename from sdk-libs/sdk/src/interface/program/decompression/pda.rs rename to sdk-libs/sdk-types/src/interface/program/decompression/pda.rs index 60f9f7c4c3..2cbf6ba77a 100644 --- a/sdk-libs/sdk/src/interface/program/decompression/pda.rs +++ b/sdk-libs/sdk-types/src/interface/program/decompression/pda.rs @@ -1,22 +1,29 @@ -use anchor_lang::prelude::*; +//! Generic prepare_account_for_decompression. + +use alloc::vec::Vec; + +use light_account_checks::AccountInfoTrait; use light_compressed_account::{ address::derive_address, compressed_account::PackedMerkleContext, instruction_data::with_account_info::{CompressedAccountInfo, InAccountInfo, OutAccountInfo}, }; use light_compressible::DECOMPRESSED_PDA_DISCRIMINATOR; -use light_hasher::{sha256::Sha256BE, Hasher, Sha256}; -use light_sdk_types::{constants::RENT_SPONSOR_SEED, instruction::PackedStateTreeInfo}; -use solana_account_info::AccountInfo; -use solana_program_error::ProgramError; -use solana_pubkey::Pubkey; +use light_hasher::{sha256::Sha256BE, Hasher}; use crate::{ + constants::RENT_SPONSOR_SEED, + error::LightSdkTypesError, + instruction::PackedStateTreeInfo, interface::{ - create_pda_account, DecompressCtx, LightAccount, LightAccountVariantTrait, - PackedLightAccountVariantTrait, + account::light_account::LightAccount, + program::{ + decompression::processor::DecompressCtx, + variant::{LightAccountVariantTrait, PackedLightAccountVariantTrait}, + }, }, - LightDiscriminator, + light_account_checks::discriminator::Discriminator as LightDiscriminator, + AnchorSerialize, }; /// Generic prepare_account_for_decompression. @@ -37,27 +44,30 @@ use crate::{ /// # Type Parameters /// * `SEED_COUNT` - Number of seeds including bump /// * `P` - Packed variant type implementing PackedLightAccountVariantTrait -pub fn prepare_account_for_decompression<'info, const SEED_COUNT: usize, P>( +/// * `AI` - Account info type (solana or pinocchio) +#[inline(never)] +pub fn prepare_account_for_decompression( packed: &P, tree_info: &PackedStateTreeInfo, output_queue_index: u8, - pda_account: &AccountInfo<'info>, - ctx: &mut DecompressCtx<'_, 'info>, -) -> std::result::Result<(), ProgramError> + pda_account: &AI, + ctx: &mut DecompressCtx<'_, AI>, +) -> Result<(), LightSdkTypesError> where + AI: AccountInfoTrait + Clone, P: PackedLightAccountVariantTrait, - >::Data: - LightAccount + LightDiscriminator + Clone + AnchorSerialize + AnchorDeserialize, + >::Data: LightAccount, { + // Type alias for the account data type + type Data = + <

>::Unpacked as LightAccountVariantTrait>::Data; + // 1. Unpack to get seeds (must happen first for PDA validation) - let packed_accounts = ctx - .cpi_accounts - .packed_accounts() - .map_err(|_| ProgramError::NotEnoughAccountKeys)?; + let packed_accounts = ctx.cpi_accounts.packed_accounts()?; let unpacked = packed .unpack(packed_accounts) - .map_err(|_| ProgramError::InvalidAccountData)?; + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; let account_data = unpacked.data().clone(); // 2. Get seeds from unpacked variant using seed_vec() (owned data, no lifetime issues) @@ -69,61 +79,54 @@ where // 3. SECURITY: Validate PDA derivation FIRST (defense-in-depth) // This MUST run before idempotency check to prevent accepting wrong PDAs - let expected_pda = Pubkey::create_program_address(&seed_slices, ctx.program_id) - .map_err(|_| ProgramError::InvalidSeeds)?; - - if pda_account.key != &expected_pda { - solana_msg::msg!( - "PDA key mismatch: expected {:?}, got {:?}", - expected_pda, - pda_account.key - ); - return Err(ProgramError::InvalidSeeds); + let expected_pda = AI::create_program_address(&seed_slices, ctx.program_id) + .map_err(|_| LightSdkTypesError::InvalidSeeds)?; + + if pda_account.key() != expected_pda { + return Err(LightSdkTypesError::InvalidSeeds); } // 4. Idempotency check - if PDA already has data (non-zero discriminator), skip // IMPORTANT: This runs AFTER PDA validation so wrong PDAs cannot bypass validation - if crate::interface::validation::is_pda_initialized(pda_account)? { + if crate::interface::program::validation::is_pda_initialized(pda_account)? { return Ok(()); } // 5. Hash with canonical CompressionInfo::compressed() for input verification let data_bytes = account_data .try_to_vec() - .map_err(|_| ProgramError::InvalidAccountData)?; + .map_err(|_| LightSdkTypesError::Borsh)?; let data_len = data_bytes.len(); - let mut input_data_hash = Sha256::hash(&data_bytes).map_err(|_| ProgramError::Custom(100))?; + let mut input_data_hash = Sha256BE::hash(&data_bytes)?; input_data_hash[0] = 0; // Zero first byte per protocol convention // 6. Calculate space and create PDA - type Data = - <

>::Unpacked as LightAccountVariantTrait>::Data; let discriminator_len = 8; let space = discriminator_len + data_len.max( as LightAccount>::INIT_SPACE); - let rent_minimum = ctx.rent.minimum_balance(space); + let rent_minimum = AI::get_min_rent_balance(space)?; - let system_program = ctx - .cpi_accounts - .system_program() - .map_err(|_| ProgramError::InvalidAccountData)?; + let system_program = ctx.cpi_accounts.system_program()?; // Construct rent sponsor seeds for PDA signing let rent_sponsor_bump_bytes = [ctx.rent_sponsor_bump]; let rent_sponsor_seeds: &[&[u8]] = &[RENT_SPONSOR_SEED, &rent_sponsor_bump_bytes]; - create_pda_account( - ctx.rent_sponsor, - rent_sponsor_seeds, - pda_account, - rent_minimum, - space as u64, - ctx.program_id, - &seed_slices, - system_program, - )?; + pda_account + .create_pda_account( + rent_minimum, + space as u64, + ctx.program_id, + &seed_slices, + ctx.rent_sponsor, + rent_sponsor_seeds, + system_program, + ) + .map_err(|e| LightSdkTypesError::ProgramError(e.into()))?; // 7. Write discriminator + data to PDA - let mut pda_data = pda_account.try_borrow_mut_data()?; + let mut pda_data = pda_account + .try_borrow_mut_data() + .map_err(|_| LightSdkTypesError::ConstraintViolation)?; pda_data[..8] .copy_from_slice(& as LightDiscriminator>::LIGHT_DISCRIMINATOR); @@ -133,14 +136,11 @@ where let writer = &mut &mut pda_data[8..]; decompressed .serialize(writer) - .map_err(|_| ProgramError::InvalidAccountData)?; + .map_err(|_| LightSdkTypesError::Borsh)?; - // 9. Derive compressed address from PDA key (saves instruction data size) - let address = derive_address( - &pda_account.key.to_bytes(), - &ctx.light_config.address_space[0].to_bytes(), - &ctx.program_id.to_bytes(), - ); + // 9. Derive compressed address from PDA key + let pda_key = pda_account.key(); + let address = derive_address(&pda_key, &ctx.light_config.address_space[0], ctx.program_id); // 10. Build CompressedAccountInfo for CPI let input = InAccountInfo { @@ -159,9 +159,8 @@ where // Output is a DECOMPRESSED_PDA placeholder (same as init creates). // This allows CompressAccountsIdempotent to re-compress the account // in a future cycle by finding and nullifying this placeholder. - let pda_pubkey_bytes = pda_account.key.to_bytes(); - let output_data_hash = - Sha256BE::hash(&pda_pubkey_bytes).map_err(|_| ProgramError::Custom(101))?; + let pda_pubkey_bytes = pda_account.key(); + let output_data_hash = Sha256BE::hash(&pda_pubkey_bytes)?; let output = OutAccountInfo { lamports: 0, output_merkle_tree_index: output_queue_index, diff --git a/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs b/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs new file mode 100644 index 0000000000..ad7a7475fe --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/program/decompression/processor.rs @@ -0,0 +1,572 @@ +//! Decompression instruction processor. + +#[cfg(feature = "token")] +use alloc::vec; +use alloc::vec::Vec; + +use light_account_checks::AccountInfoTrait; +#[cfg(feature = "token")] +use light_account_checks::CpiMeta; +#[cfg(feature = "token")] +use light_compressed_account::instruction_data::cpi_context::CompressedCpiContext; +use light_compressed_account::instruction_data::{ + compressed_proof::ValidityProof, + with_account_info::{CompressedAccountInfo, InstructionDataInvokeCpiWithAccountInfo}, +}; +#[cfg(feature = "token")] +use light_token_interface::{ + instructions::{ + extensions::ExtensionInstructionData, + transfer2::{ + CompressedTokenInstructionDataTransfer2, Compression, MultiInputTokenDataWithContext, + }, + }, + CPI_AUTHORITY, LIGHT_TOKEN_PROGRAM_ID, TRANSFER2, +}; + +#[cfg(feature = "token")] +use crate::{ + constants::{ + ACCOUNT_COMPRESSION_AUTHORITY_PDA, ACCOUNT_COMPRESSION_PROGRAM_ID, LIGHT_SYSTEM_PROGRAM_ID, + REGISTERED_PROGRAM_PDA, + }, + cpi_accounts::CpiAccountsConfig, + cpi_context_write::CpiContextWriteAccounts, +}; +use crate::{ + cpi_accounts::v2::CpiAccounts, + error::LightSdkTypesError, + instruction::PackedStateTreeInfo, + interface::{ + account::compression_info::CompressedAccountData, cpi::InvokeLightSystemProgram, + program::config::LightConfig, + }, + AnchorDeserialize, AnchorSerialize, CpiSigner, +}; + +/// Account indices within remaining_accounts for decompress instructions. +const FEE_PAYER_INDEX: usize = 0; +const CONFIG_INDEX: usize = 1; +const RENT_SPONSOR_INDEX: usize = 2; + +// ============================================================================ +// DecompressVariant Trait +// ============================================================================ + +/// Trait for packed program account variants that support decompression. +/// +/// Implemented by the program's `PackedProgramAccountVariant` enum +/// to handle type-specific dispatch during decompression. +/// +/// MACRO-GENERATED: The implementation contains a match statement routing each +/// enum variant to the appropriate `prepare_account_for_decompression` call. +pub trait DecompressVariant: + AnchorSerialize + AnchorDeserialize + Clone +{ + /// Decompress this variant into a PDA account. + /// + /// The implementation should match on the enum variant and call + /// `prepare_account_for_decompression::(packed, pda_account, ctx)`. + fn decompress( + &self, + meta: &PackedStateTreeInfo, + pda_account: &AI, + ctx: &mut DecompressCtx<'_, AI>, + ) -> Result<(), LightSdkTypesError>; +} + +// ============================================================================ +// Parameters and Context +// ============================================================================ + +/// Parameters for decompress_idempotent instruction. +/// Generic over the variant type - each program defines its own `PackedProgramAccountVariant`. +/// +/// Field order matches `LoadAccountsData` from light-client for compatibility. +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct DecompressIdempotentParams +where + V: AnchorSerialize + AnchorDeserialize + Clone, +{ + /// Offset into remaining_accounts where Light system accounts begin + pub system_accounts_offset: u8, + /// Accounts before this offset are PDA accounts, at and after are token accounts. + /// Set to accounts.len() if no token accounts. + pub token_accounts_offset: u8, + /// Packed index of the output queue in remaining_accounts. + pub output_queue_index: u8, + /// Validity proof for compressed account verification + pub proof: ValidityProof, + /// Accounts to decompress - wrapped in CompressedAccountData for metadata + pub accounts: Vec>, +} + +/// Context struct holding all data needed for decompression. +/// Generic over AccountInfoTrait to work with both solana and pinocchio. +pub struct DecompressCtx<'a, AI: AccountInfoTrait + Clone> { + pub program_id: &'a [u8; 32], + pub cpi_accounts: &'a CpiAccounts<'a, AI>, + pub remaining_accounts: &'a [AI], + pub rent_sponsor: &'a AI, + /// Rent sponsor PDA bump for signing + pub rent_sponsor_bump: u8, + pub light_config: &'a LightConfig, + pub current_slot: u64, + /// Packed index of the output queue in remaining_accounts. + pub output_queue_index: u8, + /// Internal vec - dispatch functions push results here + pub compressed_account_infos: Vec, + // Token-specific fields (only present when token feature is enabled) + #[cfg(feature = "token")] + pub ctoken_rent_sponsor: Option<&'a AI>, + #[cfg(feature = "token")] + pub ctoken_compressible_config: Option<&'a AI>, + #[cfg(feature = "token")] + pub in_token_data: Vec, + #[cfg(feature = "token")] + pub in_tlv: Option>>, + #[cfg(feature = "token")] + pub token_seeds: Vec>, +} + +// ============================================================================ +// PDA-only Processor +// ============================================================================ + +/// Process decompression for PDA accounts (idempotent, PDA-only). +/// +/// Iterates over PDA accounts, dispatches each for decompression via `DecompressVariant`, +/// then invokes the Light system program CPI to commit compressed state. +/// +/// Idempotent: if a PDA is already initialized, it is silently skipped. +/// +/// # Account layout in remaining_accounts: +/// - `[0]`: fee_payer (Signer, mut) +/// - `[1]`: config (LightConfig PDA) +/// - `[2]`: rent_sponsor (mut) +/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts +/// - `[hot_accounts_start..]`: PDA accounts to decompress into +#[inline(never)] +pub fn process_decompress_pda_accounts_idempotent( + remaining_accounts: &[AI], + params: &DecompressIdempotentParams, + cpi_signer: CpiSigner, + program_id: &[u8; 32], + current_slot: u64, +) -> Result<(), LightSdkTypesError> +where + AI: AccountInfoTrait + Clone, + V: DecompressVariant, +{ + let system_accounts_offset = params.system_accounts_offset as usize; + if system_accounts_offset > remaining_accounts.len() { + return Err(LightSdkTypesError::InvalidInstructionData); + } + + // PDA accounts: all accounts up to token_accounts_offset + let num_pda_accounts = params.token_accounts_offset as usize; + let pda_accounts = params + .accounts + .get(..num_pda_accounts) + .ok_or(LightSdkTypesError::InvalidInstructionData)?; + + if pda_accounts.is_empty() { + return Err(LightSdkTypesError::InvalidInstructionData); + } + + // 2. Load and validate config + let config = LightConfig::load_checked(&remaining_accounts[CONFIG_INDEX], program_id)?; + let rent_sponsor = &remaining_accounts[RENT_SPONSOR_INDEX]; + let rent_sponsor_bump = config.validate_rent_sponsor_account::(rent_sponsor)?; + + // 3. Hot accounts (PDAs) at the tail of remaining_accounts + let num_hot_accounts = params.accounts.len(); + let hot_accounts_start = remaining_accounts + .len() + .checked_sub(num_hot_accounts) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + let hot_account_infos = &remaining_accounts[hot_accounts_start..]; + let pda_account_infos = hot_account_infos + .get(..num_pda_accounts) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + + // 4. Build CpiAccounts (system + tree accounts, excluding hot accounts) + let cpi_accounts = CpiAccounts::new( + &remaining_accounts[FEE_PAYER_INDEX], + &remaining_accounts[system_accounts_offset..hot_accounts_start], + cpi_signer, + ); + + // 5. Build context and dispatch (scoped to release borrows before CPI) + let compressed_account_infos = { + let mut decompress_ctx = DecompressCtx { + program_id, + cpi_accounts: &cpi_accounts, + remaining_accounts, + rent_sponsor, + rent_sponsor_bump, + light_config: &config, + current_slot, + output_queue_index: params.output_queue_index, + compressed_account_infos: Vec::with_capacity(num_pda_accounts), + #[cfg(feature = "token")] + ctoken_rent_sponsor: None, + #[cfg(feature = "token")] + ctoken_compressible_config: None, + #[cfg(feature = "token")] + in_token_data: Vec::new(), + #[cfg(feature = "token")] + in_tlv: None, + #[cfg(feature = "token")] + token_seeds: Vec::new(), + }; + + for (pda_account_data, pda_account_info) in pda_accounts.iter().zip(pda_account_infos) { + pda_account_data.data.decompress( + &pda_account_data.tree_info, + pda_account_info, + &mut decompress_ctx, + )?; + } + + decompress_ctx.compressed_account_infos + }; + + // 6. If no compressed accounts were produced (all already initialized), skip CPI + if compressed_account_infos.is_empty() { + return Ok(()); + } + + // 7. Build and invoke Light system program CPI + let mut cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo::new( + program_id.into(), + cpi_signer.bump, + params.proof.into(), + ); + cpi_ix_data.account_infos = compressed_account_infos; + cpi_ix_data.invoke::(cpi_accounts)?; + + Ok(()) +} + +// ============================================================================ +// Full Processor (PDA + Token) +// ============================================================================ + +/// Process decompression for both PDA and token accounts (idempotent). +/// +/// Handles the combined PDA + token decompression flow: +/// - PDA accounts are decompressed first +/// - If both PDAs and tokens exist, PDA data is written to CPI context first +/// - Token accounts are decompressed via Transfer2 CPI to the light token program +/// +/// # Account layout in remaining_accounts: +/// - `[0]`: fee_payer (Signer, mut) +/// - `[1]`: config (LightConfig PDA) +/// - `[2]`: rent_sponsor (mut) +/// - `[3]`: ctoken_rent_sponsor (mut) +/// - `[4]`: light_token_program +/// - `[5]`: cpi_authority +/// - `[6]`: ctoken_compressible_config +/// - `[system_accounts_offset..hot_accounts_start]`: Light system + tree accounts +/// - `[hot_accounts_start..]`: Hot accounts (PDAs then tokens) +#[cfg(feature = "token")] +#[inline(never)] +pub fn process_decompress_accounts_idempotent( + remaining_accounts: &[AI], + params: &DecompressIdempotentParams, + cpi_signer: CpiSigner, + program_id: &[u8; 32], + current_slot: u64, +) -> Result<(), LightSdkTypesError> +where + AI: AccountInfoTrait + Clone, + V: DecompressVariant, +{ + let system_accounts_offset = params.system_accounts_offset as usize; + if system_accounts_offset > remaining_accounts.len() { + return Err(LightSdkTypesError::InvalidInstructionData); + } + + // 2. Split accounts into PDA and token + let (pda_accounts, token_accounts) = params + .accounts + .split_at_checked(params.token_accounts_offset as usize) + .ok_or(LightSdkTypesError::InvalidInstructionData)?; + + // 3. Load and validate config + let config = LightConfig::load_checked(&remaining_accounts[CONFIG_INDEX], program_id)?; + let rent_sponsor = &remaining_accounts[RENT_SPONSOR_INDEX]; + let rent_sponsor_bump = config.validate_rent_sponsor_account::(rent_sponsor)?; + + // 4. Hot accounts at the tail of remaining_accounts + let num_hot_accounts = params.accounts.len(); + let hot_accounts_start = remaining_accounts + .len() + .checked_sub(num_hot_accounts) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + let hot_account_infos = &remaining_accounts[hot_accounts_start..]; + let (pda_account_infos, token_account_infos) = hot_account_infos + .split_at_checked(params.token_accounts_offset as usize) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + + let has_pda_accounts = !pda_accounts.is_empty(); + let has_token_accounts = !token_accounts.is_empty(); + let cpi_context = has_pda_accounts && has_token_accounts; + + // 5. Build CpiAccounts + let cpi_config = CpiAccountsConfig { + sol_compression_recipient: false, + sol_pool_pda: false, + cpi_context, + cpi_signer, + }; + let cpi_accounts = CpiAccounts::new_with_config( + &remaining_accounts[FEE_PAYER_INDEX], + &remaining_accounts[system_accounts_offset..hot_accounts_start], + cpi_config, + ); + + // Token (ctoken) accounts layout (only required when token accounts are present): + // [3] ctoken_rent_sponsor, [6] ctoken_compressible_config + let (ctoken_rent_sponsor, ctoken_compressible_config) = if has_token_accounts { + let rent_sponsor = remaining_accounts + .get(3) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + let config = remaining_accounts + .get(6) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + (Some(rent_sponsor), Some(config)) + } else { + (None, None) + }; + + // 6. Build context and dispatch (scoped to release borrows before CPI) + let (compressed_account_infos, in_token_data, in_tlv, token_seeds) = { + let mut decompress_ctx = DecompressCtx { + program_id, + cpi_accounts: &cpi_accounts, + remaining_accounts, + rent_sponsor, + rent_sponsor_bump, + light_config: &config, + current_slot, + output_queue_index: params.output_queue_index, + compressed_account_infos: Vec::new(), + ctoken_rent_sponsor, + ctoken_compressible_config, + in_token_data: Vec::new(), + in_tlv: None, + token_seeds: Vec::new(), + }; + + // Process PDA accounts + for (pda_account_data, pda_account_info) in pda_accounts.iter().zip(pda_account_infos) { + pda_account_data.data.decompress( + &pda_account_data.tree_info, + pda_account_info, + &mut decompress_ctx, + )?; + } + + // Process token accounts + for (token_account_data, token_account_info) in + token_accounts.iter().zip(token_account_infos) + { + token_account_data.data.decompress( + &token_account_data.tree_info, + token_account_info, + &mut decompress_ctx, + )?; + } + + ( + decompress_ctx.compressed_account_infos, + decompress_ctx.in_token_data, + decompress_ctx.in_tlv, + decompress_ctx.token_seeds, + ) + }; + + // 7. PDA CPI (Light system program) + if has_pda_accounts { + let pda_only = !cpi_context; + + if pda_only { + let mut cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo::new( + program_id.into(), + cpi_signer.bump, + params.proof.into(), + ); + cpi_ix_data.account_infos = compressed_account_infos; + cpi_ix_data.invoke::(cpi_accounts.clone())?; + } else { + // PDAs + tokens: write PDA data to CPI context first, tokens will execute + let authority = cpi_accounts.authority()?; + let cpi_context_account = cpi_accounts.cpi_context()?; + let system_cpi_accounts = CpiContextWriteAccounts { + fee_payer: &remaining_accounts[FEE_PAYER_INDEX], + authority, + cpi_context: cpi_context_account, + cpi_signer, + }; + + let cpi_ix_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: cpi_signer.bump, + invoking_program_id: cpi_signer.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: true, + with_transaction_hash: false, + cpi_context: CompressedCpiContext::first(), + proof: None, + new_address_params: Vec::new(), + account_infos: compressed_account_infos, + read_only_addresses: Vec::new(), + read_only_accounts: Vec::new(), + }; + cpi_ix_data.invoke_write_to_cpi_context_first(system_cpi_accounts)?; + } + } + + // 8. Token CPI (Transfer2 to light token program) + if has_token_accounts { + let mut compressions = Vec::new(); + for a in &in_token_data { + compressions.push(Compression::decompress(a.amount, a.mint, a.owner)); + } + + let mut cpi = CompressedTokenInstructionDataTransfer2 { + with_transaction_hash: false, + in_token_data: in_token_data.clone(), + in_tlv: in_tlv.clone(), + with_lamports_change_account_merkle_tree_index: false, + lamports_change_account_merkle_tree_index: 0, + lamports_change_account_owner_index: 0, + output_queue: 0, + max_top_up: 0, + cpi_context: None, + compressions: Some(compressions), + proof: params.proof.0, + out_token_data: Vec::new(), + in_lamports: None, + out_lamports: None, + out_tlv: None, + }; + + if has_pda_accounts { + cpi.cpi_context = Some( + light_token_interface::instructions::transfer2::CompressedCpiContext { + set_context: false, + first_set_context: false, + }, + ); + } + + // Build Transfer2 account metas in the order the handler expects: + // [0] light_system_program (readonly) + // [1] fee_payer (signer, writable) + // [2] cpi_authority_pda (readonly) + // [3] registered_program_pda (readonly) + // [4] account_compression_authority (readonly) + // [5] account_compression_program (readonly) + // [6] system_program (readonly) + // [7] cpi_context (optional, writable) + // [N+] packed_accounts + let fee_payer_key = remaining_accounts[FEE_PAYER_INDEX].key(); + let mut account_metas = vec![ + CpiMeta { + pubkey: LIGHT_SYSTEM_PROGRAM_ID, + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: fee_payer_key, + is_signer: true, + is_writable: true, + }, + CpiMeta { + pubkey: CPI_AUTHORITY, + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: REGISTERED_PROGRAM_PDA, + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: ACCOUNT_COMPRESSION_AUTHORITY_PDA, + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: ACCOUNT_COMPRESSION_PROGRAM_ID, + is_signer: false, + is_writable: false, + }, + CpiMeta { + pubkey: [0u8; 32], + is_signer: false, + is_writable: false, + }, // system_program + ]; + + if cpi_context { + let cpi_ctx = cpi_accounts.cpi_context()?; + account_metas.push(CpiMeta { + pubkey: cpi_ctx.key(), + is_signer: false, + is_writable: true, + }); + } + + let transfer2_packed_start = account_metas.len(); + let packed_accounts_offset = + system_accounts_offset + cpi_accounts.system_accounts_end_offset(); + for account in &remaining_accounts[packed_accounts_offset..] { + account_metas.push(CpiMeta { + pubkey: account.key(), + is_signer: account.is_signer(), + is_writable: account.is_writable(), + }); + } + + // Mark owner accounts as signers for the Transfer2 CPI + for data in &in_token_data { + account_metas[data.owner as usize + transfer2_packed_start].is_signer = true; + } + + // Serialize instruction data + let mut transfer2_data = vec![TRANSFER2]; + cpi.serialize(&mut transfer2_data) + .map_err(|_| LightSdkTypesError::Borsh)?; + + // Invoke the light token program + if token_seeds.is_empty() { + // All ATAs - no PDA signing needed + AI::invoke_cpi( + &LIGHT_TOKEN_PROGRAM_ID, + &transfer2_data, + &account_metas, + remaining_accounts, + &[], + ) + .map_err(|e| LightSdkTypesError::ProgramError(e.into()))?; + } else { + // At least one regular token account - use invoke_signed with PDA seeds + let signer_seed_refs: Vec<&[u8]> = token_seeds.iter().map(|s| s.as_slice()).collect(); + AI::invoke_cpi( + &LIGHT_TOKEN_PROGRAM_ID, + &transfer2_data, + &account_metas, + remaining_accounts, + &[signer_seed_refs.as_slice()], + ) + .map_err(|e| LightSdkTypesError::ProgramError(e.into()))?; + } + } + + Ok(()) +} diff --git a/sdk-libs/sdk/src/interface/program/decompression/token.rs b/sdk-libs/sdk-types/src/interface/program/decompression/token.rs similarity index 50% rename from sdk-libs/sdk/src/interface/program/decompression/token.rs rename to sdk-libs/sdk-types/src/interface/program/decompression/token.rs index f1cef2f13c..613feed142 100644 --- a/sdk-libs/sdk/src/interface/program/decompression/token.rs +++ b/sdk-libs/sdk-types/src/interface/program/decompression/token.rs @@ -1,29 +1,35 @@ //! Token account decompression. -use light_sdk_types::instruction::PackedStateTreeInfo; -use light_token_interface::instructions::extensions::ExtensionInstructionData; -use solana_account_info::AccountInfo; -use solana_program_error::ProgramError; +use alloc::{vec, vec::Vec}; + +use light_account_checks::AccountInfoTrait; +use light_token_interface::{ + instructions::extensions::ExtensionInstructionData, LIGHT_TOKEN_PROGRAM_ID, +}; use super::create_token_account::{ build_create_ata_instruction, build_create_token_account_instruction, }; -use crate::interface::{DecompressCtx, PackedLightAccountVariantTrait}; +use crate::{ + error::LightSdkTypesError, + instruction::PackedStateTreeInfo, + interface::program::{ + decompression::processor::DecompressCtx, variant::PackedLightAccountVariantTrait, + }, +}; -pub fn prepare_token_account_for_decompression<'info, const SEED_COUNT: usize, P>( +pub fn prepare_token_account_for_decompression( packed: &P, tree_info: &PackedStateTreeInfo, output_queue_index: u8, - token_account_info: &AccountInfo<'info>, - ctx: &mut DecompressCtx<'_, 'info>, -) -> std::result::Result<(), ProgramError> + token_account_info: &AI, + ctx: &mut DecompressCtx<'_, AI>, +) -> Result<(), LightSdkTypesError> where + AI: AccountInfoTrait + Clone, P: PackedLightAccountVariantTrait, { - let packed_accounts = ctx - .cpi_accounts - .packed_accounts() - .map_err(|_| ProgramError::NotEnoughAccountKeys)?; + let packed_accounts = ctx.cpi_accounts.packed_accounts()?; let token_data = packed.into_in_token_data(tree_info, output_queue_index)?; // Get TLV extension early to detect ATA @@ -45,83 +51,103 @@ where }); // Resolve mint pubkey from packed index - let mint_pubkey = packed_accounts + let mint_key = packed_accounts .get(token_data.mint as usize) - .ok_or(ProgramError::InvalidAccountData)? - .key; + .ok_or(LightSdkTypesError::InvalidInstructionData)? + .key(); - let fee_payer = ctx.cpi_accounts.fee_payer(); + let fee_payer_key = ctx.cpi_accounts.fee_payer().key(); - // Helper to check if token account is already initialized + // Idempotency: check if token account is already initialized // State byte at offset 108: 0=Uninitialized, 1=Initialized, 2=Frozen const STATE_OFFSET: usize = 108; - let is_already_initialized = !token_account_info.data_is_empty() - && token_account_info.data_len() > STATE_OFFSET - && token_account_info.try_borrow_data()?[STATE_OFFSET] != 0; + let is_already_initialized = token_account_info.data_len() > STATE_OFFSET && { + let data = token_account_info + .try_borrow_data() + .map_err(|_| LightSdkTypesError::ConstraintViolation)?; + data[STATE_OFFSET] != 0 + }; + + // Get token-specific references from context + let ctoken_compressible_config_key = ctx + .ctoken_compressible_config + .as_ref() + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)? + .key(); + let ctoken_rent_sponsor_key = ctx + .ctoken_rent_sponsor + .as_ref() + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)? + .key(); if let Some((ata_bump, wallet_owner_index)) = ata_info { // ATA path: use invoke() without signer seeds - // Resolve wallet owner pubkey from packed index - let wallet_owner_pubkey = packed_accounts + let wallet_owner_key = packed_accounts .get(wallet_owner_index as usize) - .ok_or(ProgramError::InvalidAccountData)? - .key; - - // Idempotency check: only create ATA if it doesn't exist - // For ATAs, we still continue with decompression even if account exists - if token_account_info.data_is_empty() { - let instruction = build_create_ata_instruction( - wallet_owner_pubkey, - mint_pubkey, - fee_payer.key, - token_account_info.key, + .ok_or(LightSdkTypesError::InvalidInstructionData)? + .key(); + + // Idempotency: only create ATA if it doesn't exist + if token_account_info.data_len() == 0 { + let (data, account_metas) = build_create_ata_instruction( + &wallet_owner_key, + &mint_key, + &fee_payer_key, + &token_account_info.key(), ata_bump, - ctx.ctoken_compressible_config.key, - ctx.ctoken_rent_sponsor.key, + &ctoken_compressible_config_key, + &ctoken_rent_sponsor_key, ctx.light_config.write_top_up, )?; - // Invoke WITHOUT signer seeds - ATA is derived from light token program, not our program - anchor_lang::solana_program::program::invoke(&instruction, ctx.remaining_accounts)?; + // Invoke WITHOUT signer seeds - ATA is derived from light token program + AI::invoke_cpi( + &LIGHT_TOKEN_PROGRAM_ID, + &data, + &account_metas, + ctx.remaining_accounts, + &[], + ) + .map_err(|e| LightSdkTypesError::ProgramError(e.into()))?; } - // Don't extend token_seeds for ATAs (invoke, not invoke_signed) } else { // Regular token vault path: use invoke_signed with PDA seeds - // For regular vaults, if already initialized, skip BOTH creation AND decompression (full idempotency) if is_already_initialized { - solana_msg::msg!("Token vault is already decompressed, skipping"); return Ok(()); } let bump = &[packed.bump()]; let seeds = packed .seed_refs_with_bump(packed_accounts, bump) - .map_err(|_| ProgramError::InvalidSeeds)?; + .map_err(|_| LightSdkTypesError::InvalidSeeds)?; // Derive owner pubkey from constant owner_seeds let owner = packed.derive_owner(); let signer_seeds: Vec<&[u8]> = seeds.iter().copied().collect(); - let instruction = build_create_token_account_instruction( - token_account_info.key, - mint_pubkey, + let (data, account_metas) = build_create_token_account_instruction( + &token_account_info.key(), + &mint_key, &owner, - fee_payer.key, - ctx.ctoken_compressible_config.key, - ctx.ctoken_rent_sponsor.key, + &fee_payer_key, + &ctoken_compressible_config_key, + &ctoken_rent_sponsor_key, ctx.light_config.write_top_up, &signer_seeds, ctx.program_id, )?; // Invoke with PDA seeds - anchor_lang::solana_program::program::invoke_signed( - &instruction, + AI::invoke_cpi( + &LIGHT_TOKEN_PROGRAM_ID, + &data, + &account_metas, ctx.remaining_accounts, &[signer_seeds.as_slice()], - )?; + ) + .map_err(|e| LightSdkTypesError::ProgramError(e.into()))?; // Push seeds for the Transfer2 CPI (needed for invoke_signed) ctx.token_seeds.extend(seeds.iter().map(|s| s.to_vec())); diff --git a/sdk-libs/sdk/src/interface/program/mod.rs b/sdk-libs/sdk-types/src/interface/program/mod.rs similarity index 91% rename from sdk-libs/sdk/src/interface/program/mod.rs rename to sdk-libs/sdk-types/src/interface/program/mod.rs index 3e0139e7d1..123df1ee9c 100644 --- a/sdk-libs/sdk/src/interface/program/mod.rs +++ b/sdk-libs/sdk-types/src/interface/program/mod.rs @@ -5,8 +5,6 @@ pub mod compression; pub mod config; +pub mod decompression; pub mod validation; pub mod variant; - -#[cfg(feature = "anchor")] -pub mod decompression; diff --git a/sdk-libs/sdk-types/src/interface/program/validation.rs b/sdk-libs/sdk-types/src/interface/program/validation.rs new file mode 100644 index 0000000000..f8745b7381 --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/program/validation.rs @@ -0,0 +1,146 @@ +//! Shared validation utilities for compress/decompress operations. + +use light_account_checks::{ + account_iterator::AccountIterator, checks::check_data_is_zeroed, AccountInfoTrait, +}; + +use crate::{error::LightSdkTypesError, interface::program::config::LightConfig}; + +/// Validated PDA context after account extraction and config validation. +pub struct ValidatedPdaContext { + pub fee_payer: AI, + pub light_config: LightConfig, + pub rent_sponsor: AI, + pub rent_sponsor_bump: u8, + /// Only present when EXTRACT_COMPRESSION_AUTHORITY=true + pub compression_authority: Option, +} + +/// Extract and validate accounts for compress operations (4 accounts including compression_authority). +/// +/// # Account layout: +/// - `0` - fee_payer (Signer, mut) +/// - `1` - config (LightConfig PDA) +/// - `2` - rent_sponsor (mut) +/// - `3` - compression_authority +pub fn validate_compress_accounts( + remaining_accounts: &[AI], + program_id: &[u8; 32], +) -> Result, LightSdkTypesError> { + validate_pda_common_accounts_inner::(remaining_accounts, program_id) +} + +/// Extract and validate accounts for decompress operations (3 accounts, no compression_authority). +/// +/// # Account layout: +/// - `0` - fee_payer (Signer, mut) +/// - `1` - config (LightConfig PDA) +/// - `2` - rent_sponsor (mut) +pub fn validate_decompress_accounts( + remaining_accounts: &[AI], + program_id: &[u8; 32], +) -> Result, LightSdkTypesError> { + validate_pda_common_accounts_inner::(remaining_accounts, program_id) +} + +/// Internal function with const generic for optional compression_authority extraction. +fn validate_pda_common_accounts_inner( + remaining_accounts: &[AI], + program_id: &[u8; 32], +) -> Result, LightSdkTypesError> +where + AI: AccountInfoTrait + Clone, +{ + let mut account_iter = AccountIterator::new(remaining_accounts); + + let fee_payer = account_iter + .next_signer_mut("fee_payer") + .map_err(LightSdkTypesError::AccountError)?; + let config = account_iter + .next_non_mut("config") + .map_err(LightSdkTypesError::AccountError)?; + let rent_sponsor = account_iter + .next_mut("rent_sponsor") + .map_err(LightSdkTypesError::AccountError)?; + + let compression_authority = if EXTRACT_COMPRESSION_AUTHORITY { + Some( + account_iter + .next_account("compression_authority") + .map_err(LightSdkTypesError::AccountError)? + .clone(), + ) + } else { + None + }; + + let light_config = LightConfig::load_checked(config, program_id)?; + + let rent_sponsor_bump = light_config.validate_rent_sponsor_account(rent_sponsor)?; + + Ok(ValidatedPdaContext { + fee_payer: fee_payer.clone(), + light_config, + rent_sponsor: rent_sponsor.clone(), + rent_sponsor_bump, + compression_authority, + }) +} + +/// Validate and split remaining_accounts at system_accounts_offset. +/// +/// Returns (accounts_before_offset, accounts_from_offset). +pub fn split_at_system_accounts_offset( + remaining_accounts: &[AI], + system_accounts_offset: u8, +) -> Result<(&[AI], &[AI]), LightSdkTypesError> { + let offset = system_accounts_offset as usize; + remaining_accounts + .split_at_checked(offset) + .ok_or(LightSdkTypesError::ConstraintViolation) +} + +/// Extract PDA accounts from the tail of remaining_accounts. +pub fn extract_tail_accounts( + remaining_accounts: &[AI], + num_pda_accounts: usize, +) -> Result<&[AI], LightSdkTypesError> { + let start = remaining_accounts + .len() + .checked_sub(num_pda_accounts) + .ok_or(LightSdkTypesError::ConstraintViolation)?; + Ok(&remaining_accounts[start..]) +} + +/// Check if PDA account is already initialized (has non-zero discriminator). +/// +/// Returns: +/// - `Ok(true)` if account has data and non-zero discriminator (initialized) +/// - `Ok(false)` if account is empty or has zeroed discriminator (not initialized) +pub fn is_pda_initialized(account: &AI) -> Result { + use light_account_checks::discriminator::DISCRIMINATOR_LEN; + + if account.data_is_empty() { + return Ok(false); + } + let data = account + .try_borrow_data() + .map_err(|_| LightSdkTypesError::ConstraintViolation)?; + if data.len() < DISCRIMINATOR_LEN { + return Ok(false); + } + // If discriminator is NOT zeroed, account is initialized + Ok(check_data_is_zeroed::(&data).is_err()) +} + +/// Check if account should be skipped during compression. +/// +/// Returns true if: +/// - Account has no data (empty) +/// - Account is not owned by the expected program +pub fn should_skip_compression( + account: &AI, + expected_owner: &[u8; 32], +) -> bool { + account.data_is_empty() || !account.is_owned_by(expected_owner) +} diff --git a/sdk-libs/sdk-types/src/interface/program/variant.rs b/sdk-libs/sdk-types/src/interface/program/variant.rs new file mode 100644 index 0000000000..08e9a577dd --- /dev/null +++ b/sdk-libs/sdk-types/src/interface/program/variant.rs @@ -0,0 +1,195 @@ +//! Traits for decompression variant construction and Light Protocol implementation. +//! +//! This module contains traits for typed compressed account handling: +//! - Base traits (`IntoVariant`) - always available +//! - Variant traits (`LightAccountVariantTrait`, `PackedLightAccountVariantTrait`) - always available +//! - Token seed traits (`UnpackedTokenSeeds`, `PackedTokenSeeds`) - behind `token` feature + +use alloc::vec::Vec; + +use light_account_checks::AccountInfoTrait; + +use crate::{ + error::LightSdkTypesError, interface::account::light_account::AccountType, AnchorDeserialize, + AnchorSerialize, +}; + +// --- Base trait (always available) --- + +/// 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. +pub trait IntoVariant { + /// Construct variant from compressed account data bytes and these seeds. + fn into_variant(self, data: &[u8]) -> Result; +} + +// --- Variant traits --- + +/// Trait for unpacked compressed account variants with seeds. +/// +/// Implementations are generated by the `#[light_program]` macro for each +/// account type marked with `#[light_account(init)]`. +/// +/// # Type Parameters +/// * `SEED_COUNT` - Number of seeds including bump for CPI signing +pub trait LightAccountVariantTrait: + Sized + Clone + AnchorSerialize + AnchorDeserialize +{ + /// The program ID that owns accounts of this variant type. + const PROGRAM_ID: [u8; 32]; + + /// The seeds struct type containing seed values. + type Seeds; + + /// The account data type. + type Data; + + /// The packed variant type for efficient serialization. + type Packed: PackedLightAccountVariantTrait; + + /// Get a reference to the account data. + fn data(&self) -> &Self::Data; + + /// Get seed values as owned byte vectors for PDA derivation. + fn seed_vec(&self) -> Vec>; + + /// Get seed references with bump for CPI signing. + /// Returns a fixed-size array that can be passed to invoke_signed. + fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; SEED_COUNT]; + + /// Derive the PDA address and bump seed using PROGRAM_ID. + fn derive_pda(&self) -> ([u8; 32], u8) { + let seeds = self.seed_vec(); + let seed_slices: Vec<&[u8]> = seeds.iter().map(|s| s.as_slice()).collect(); + AI::find_program_address(&seed_slices, &Self::PROGRAM_ID) + } +} + +/// Trait for packed compressed account variants. +/// +/// Packed variants use u8 indices instead of 32-byte Pubkeys for efficient +/// serialization. They can be unpacked back to full variants using account info. +#[allow(clippy::wrong_self_convention)] +pub trait PackedLightAccountVariantTrait: + Sized + Clone + AnchorSerialize + AnchorDeserialize +{ + /// The unpacked variant type with full Pubkey values. + type Unpacked: LightAccountVariantTrait; + + /// The account type (Pda, Token, Ata, etc.) for dispatch. + const ACCOUNT_TYPE: AccountType; + + /// Get the PDA bump seed. + fn bump(&self) -> u8; + + /// Unpack this variant by resolving u8 indices to Pubkeys. + fn unpack( + &self, + accounts: &[AI], + ) -> Result; + + /// Get seed references with bump for CPI signing. + /// Resolves u8 indices to pubkey refs from accounts slice. + fn seed_refs_with_bump<'a, AI: AccountInfoTrait>( + &'a self, + accounts: &'a [AI], + bump_storage: &'a [u8; 1], + ) -> Result<[&'a [u8]; SEED_COUNT], LightSdkTypesError>; + + /// Extract token data for compressed token CPI. + /// Only meaningful for token account variants; PDA variants return an error. + #[cfg(feature = "token")] + fn into_in_token_data( + &self, + _tree_info: &crate::instruction::PackedStateTreeInfo, + _output_queue_index: u8, + ) -> Result< + light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext, + LightSdkTypesError, + > { + Err(LightSdkTypesError::InvalidInstructionData) + } + + /// Extract TLV extension data for compressed token CPI. + /// Only meaningful for token account variants; PDA variants return `None`. + #[cfg(feature = "token")] + fn into_in_tlv( + &self, + ) -> Result< + Option>, + LightSdkTypesError, + > { + Ok(None) + } + + /// Derive the owner pubkey from constant owner_seeds and program ID. + /// Only meaningful for token account variants; PDA variants return default. + #[cfg(feature = "token")] + fn derive_owner(&self) -> [u8; 32] { + [0u8; 32] + } +} + +// --- Token seed traits (behind `token` feature) --- + +#[cfg(feature = "token")] +mod token_traits { + use alloc::vec::Vec; + + use light_account_checks::AccountInfoTrait; + + use crate::{error::LightSdkTypesError, AnchorDeserialize, AnchorSerialize}; + + /// Trait for unpacked token seed structs. + /// + /// Generated by the `#[light_program]` macro on per-variant seed structs + /// (e.g., `TokenVaultSeeds`). Provides seed-specific behavior for the blanket + /// `LightAccountVariantTrait` impl on `TokenDataWithSeeds`. + pub trait UnpackedTokenSeeds: + Clone + core::fmt::Debug + AnchorSerialize + AnchorDeserialize + { + /// The packed seeds type. + type Packed: PackedTokenSeeds; + + const PROGRAM_ID: [u8; 32]; + fn seed_vec(&self) -> Vec>; + fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; N]; + } + + /// Trait for packed token seed structs. + /// + /// Generated by the `#[light_program]` macro on per-variant packed seed structs + /// (e.g., `PackedTokenVaultSeeds`). Provides seed-specific behavior for the blanket + /// `PackedLightAccountVariantTrait` impl on `TokenDataWithPackedSeeds`. + /// + /// Note: `Unpack` cannot be a supertrait because `AI` is not available in + /// the trait definition. Instead, generic unpack/seed methods are defined here directly. + pub trait PackedTokenSeeds: + Clone + core::fmt::Debug + AnchorSerialize + AnchorDeserialize + { + type Unpacked: UnpackedTokenSeeds; + + fn bump(&self) -> u8; + + /// Unpack seeds by resolving u8 indices to pubkeys from accounts slice. + fn unpack_seeds( + &self, + accounts: &[AI], + ) -> Result; + + /// Get seed references with bump for CPI signing. + fn seed_refs_with_bump<'a, AI: AccountInfoTrait>( + &'a self, + accounts: &'a [AI], + bump_storage: &'a [u8; 1], + ) -> Result<[&'a [u8]; N], LightSdkTypesError>; + + /// Derive the owner pubkey from constant owner_seeds and program ID. + fn derive_owner(&self) -> [u8; 32]; + } +} + +#[cfg(feature = "token")] +pub use token_traits::{PackedTokenSeeds, UnpackedTokenSeeds}; diff --git a/sdk-libs/sdk-types/src/lib.rs b/sdk-libs/sdk-types/src/lib.rs index e1c2fcae88..6f9be9cba6 100644 --- a/sdk-libs/sdk-types/src/lib.rs +++ b/sdk-libs/sdk-types/src/lib.rs @@ -12,23 +12,28 @@ #![cfg_attr(not(feature = "std"), no_std)] -#[cfg(all(not(feature = "std"), feature = "alloc"))] +#[cfg(feature = "alloc")] extern crate alloc; pub mod address; pub mod constants; pub mod cpi_accounts; -#[cfg(feature = "cpi-context")] pub mod cpi_context_write; pub mod error; pub mod instruction; +#[cfg(feature = "std")] +pub mod pack_accounts; + +#[cfg(all(feature = "alloc", feature = "v2"))] +pub mod interface; // Re-exports #[cfg(feature = "anchor")] -use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +pub use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] -use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +pub use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; pub use constants::*; +pub use light_account_checks::{self, discriminator::Discriminator as LightDiscriminator}; pub use light_compressed_account::CpiSigner; #[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize)] diff --git a/sdk-libs/sdk-types/src/pack_accounts.rs b/sdk-libs/sdk-types/src/pack_accounts.rs new file mode 100644 index 0000000000..03faedff85 --- /dev/null +++ b/sdk-libs/sdk-types/src/pack_accounts.rs @@ -0,0 +1,178 @@ +//! Utilities for packing accounts into instruction data. +//! +//! [`PackedAccounts`] is a builder for efficiently organizing accounts into the three categories +//! required for compressed account instructions: +//! 1. **Pre-accounts** - Custom accounts needed before system accounts +//! 2. **System accounts** - Static light system program accounts +//! 3. **Packed accounts** - Dynamically packed accounts (Merkle trees, address trees, queues) with automatic deduplication + +use std::collections::HashMap; + +use light_account_checks::AccountMetaTrait; + +use crate::error::LightSdkTypesError; + +/// Builder to collect accounts for compressed account instructions. +/// +/// Generic over `AM: AccountMetaTrait` to work with both solana and pinocchio account metas. +/// +/// Manages three categories of accounts: +/// - **Pre-accounts**: Signers and other custom accounts that come before system accounts. +/// - **System accounts**: Light system program accounts (authority, trees, queues). +/// - **Packed accounts**: Dynamically tracked deduplicated accounts. +#[derive(Debug)] +pub struct PackedAccounts { + /// Accounts that must come before system accounts (e.g., signers, fee payer). + pub pre_accounts: Vec, + /// Light system program accounts (authority, programs, trees, queues). + system_accounts: Vec, + /// Next available index for packed accounts. + next_index: u8, + /// Map of pubkey bytes to (index, AccountMeta) for deduplication and index tracking. + map: HashMap<[u8; 32], (u8, AM)>, + /// Field to sanity check + system_accounts_set: bool, +} + +impl Default for PackedAccounts { + fn default() -> Self { + Self { + pre_accounts: Vec::new(), + system_accounts: Vec::new(), + next_index: 0, + map: HashMap::new(), + system_accounts_set: false, + } + } +} + +impl PackedAccounts { + pub fn system_accounts_set(&self) -> bool { + self.system_accounts_set + } + + pub fn add_pre_accounts_signer(&mut self, pubkey: AM::Pubkey) { + self.pre_accounts.push(AM::new(pubkey, true, false)); + } + + pub fn add_pre_accounts_signer_mut(&mut self, pubkey: AM::Pubkey) { + self.pre_accounts.push(AM::new(pubkey, true, true)); + } + + pub fn add_pre_accounts_meta(&mut self, account_meta: AM) { + self.pre_accounts.push(account_meta); + } + + pub fn add_pre_accounts_metas(&mut self, account_metas: &[AM]) { + self.pre_accounts.extend_from_slice(account_metas); + } + + pub fn add_system_accounts_raw(&mut self, system_accounts: Vec) { + self.system_accounts.extend(system_accounts); + self.system_accounts_set = true; + } + + /// Returns the index of the provided `pubkey` in the collection. + /// + /// If the provided `pubkey` is not a part of the collection, it gets + /// inserted with a `next_index`. + /// + /// If the provided `pubkey` already exists in the collection, its already + /// existing index is returned. + pub fn insert_or_get(&mut self, pubkey: AM::Pubkey) -> u8 { + self.insert_or_get_config(pubkey, false, true) + } + + pub fn insert_or_get_read_only(&mut self, pubkey: AM::Pubkey) -> u8 { + self.insert_or_get_config(pubkey, false, false) + } + + pub fn insert_or_get_config( + &mut self, + pubkey: AM::Pubkey, + is_signer: bool, + is_writable: bool, + ) -> u8 { + let bytes = AM::pubkey_to_bytes(pubkey); + match self.map.get_mut(&bytes) { + Some((index, entry)) => { + if !entry.is_writable() { + entry.set_is_writable(is_writable); + } + if !entry.is_signer() { + entry.set_is_signer(is_signer); + } + *index + } + None => { + let index = self.next_index; + self.next_index += 1; + self.map + .insert(bytes, (index, AM::new(pubkey, is_signer, is_writable))); + index + } + } + } + + fn hash_set_accounts_to_metas(&self) -> Vec { + let mut packed_accounts = self.map.iter().collect::>(); + // hash maps are not sorted so we need to sort manually and collect into a vector again + packed_accounts.sort_by(|a, b| a.1 .0.cmp(&b.1 .0)); + packed_accounts + .iter() + .map(|(_, (_, k))| k.clone()) + .collect::>() + } + + fn get_offsets(&self) -> (usize, usize) { + let system_accounts_start_offset = self.pre_accounts.len(); + let packed_accounts_start_offset = + system_accounts_start_offset + self.system_accounts.len(); + (system_accounts_start_offset, packed_accounts_start_offset) + } + + /// Converts the collection of accounts to a vector of account metas, + /// which can be used as remaining accounts in instructions or CPI calls. + /// + /// # Returns + /// + /// A tuple of `(account_metas, system_accounts_offset, packed_accounts_offset)`: + /// - `account_metas`: All accounts concatenated in order: `[pre_accounts][system_accounts][packed_accounts]` + /// - `system_accounts_offset`: Index where system accounts start (= pre_accounts.len()) + /// - `packed_accounts_offset`: Index where packed accounts start (= pre_accounts.len() + system_accounts.len()) + pub fn to_account_metas(&self) -> (Vec, usize, usize) { + let packed_accounts = self.hash_set_accounts_to_metas(); + let (system_accounts_start_offset, packed_accounts_start_offset) = self.get_offsets(); + ( + [ + self.pre_accounts.clone(), + self.system_accounts.clone(), + packed_accounts, + ] + .concat(), + system_accounts_start_offset, + packed_accounts_start_offset, + ) + } + + pub fn packed_pubkeys(&self) -> Vec<[u8; 32]> { + self.hash_set_accounts_to_metas() + .iter() + .map(|meta| meta.pubkey_bytes()) + .collect() + } + + pub fn add_custom_system_accounts>( + &mut self, + accounts: T, + ) -> Result<(), LightSdkTypesError> { + accounts.get_account_metas_vec(self) + } +} + +pub trait AccountMetasVec { + fn get_account_metas_vec( + &self, + accounts: &mut PackedAccounts, + ) -> Result<(), LightSdkTypesError>; +} diff --git a/sdk-libs/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index a65c91e6db..eb8109cb82 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -16,7 +16,6 @@ idl-build = [ "anchor-lang/idl-build", "light-compressed-account/idl-build", "light-sdk-types/idl-build", - "light-compressible/idl-build", "light-token-interface/idl-build", "anchor", "dep:solana-program" @@ -25,15 +24,14 @@ anchor = [ "anchor-lang", "light-compressed-account/anchor", "light-sdk-types/anchor", - "light-compressible/anchor", - "light-token-interface/anchor" + "light-token-interface/anchor", ] v2 = ["light-sdk-types/v2"] cpi-context = ["light-sdk-types/cpi-context"] devnet = [] -poseidon = ["light-hasher/poseidon", "light-compressed-account/poseidon"] -keccak = ["light-hasher/keccak", "light-compressed-account/keccak"] -sha256 = ["light-hasher/sha256", "light-compressed-account/sha256"] +poseidon = ["light-hasher/poseidon", "light-compressed-account/poseidon", "light-sdk-types/poseidon"] +keccak = ["light-hasher/keccak", "light-compressed-account/keccak", "light-sdk-types/keccak"] +sha256 = ["light-hasher/sha256", "light-compressed-account/sha256", "light-sdk-types/sha256"] merkle-tree = ["light-concurrent-merkle-tree/solana"] custom-heap = ["light-heap"] profile-program = [ @@ -64,14 +62,13 @@ bincode = "1" light-program-profiler = { workspace = true } light-sdk-macros = { workspace = true } -light-sdk-types = { workspace = true, features = ["std"] } +light-sdk-types = { workspace = true, features = ["std", "token"] } light-macros = { workspace = true } light-compressed-account = { workspace = true, features = ["std"] } light-hasher = { workspace = true, features = ["std"] } light-account-checks = { workspace = true, features = ["solana"] } light-zero-copy = { workspace = true } light-concurrent-merkle-tree = { workspace = true, optional = true } -light-compressible = { workspace = true } light-heap = { workspace = true, optional = true } light-token-interface = { workspace = true } # TODO: make optional diff --git a/sdk-libs/sdk/src/cpi/invoke.rs b/sdk-libs/sdk/src/cpi/invoke.rs index 4562d0ad18..7a56d08355 100644 --- a/sdk-libs/sdk/src/cpi/invoke.rs +++ b/sdk-libs/sdk/src/cpi/invoke.rs @@ -1,12 +1,15 @@ pub use light_compressed_account::LightInstructionData; use light_sdk_types::constants::{CPI_AUTHORITY_PDA_SEED, LIGHT_SYSTEM_PROGRAM_ID}; - +use solana_account_info::AccountInfo; +use solana_cpi::invoke_signed; #[cfg(feature = "cpi-context")] -use crate::AccountMeta; +use solana_instruction::AccountMeta; +use solana_instruction::Instruction; +use solana_program_error::ProgramError; + use crate::{ cpi::{account::CpiAccountsTrait, instruction::LightCpiInstruction}, error::LightSdkError, - invoke_signed, AccountInfo, Instruction, ProgramError, }; pub trait InvokeLightSystemProgram { diff --git a/sdk-libs/sdk/src/cpi/mod.rs b/sdk-libs/sdk/src/cpi/mod.rs index c5392e5946..9c6379fa81 100644 --- a/sdk-libs/sdk/src/cpi/mod.rs +++ b/sdk-libs/sdk/src/cpi/mod.rs @@ -39,15 +39,13 @@ mod account; mod instruction; pub mod invoke; +// Re-export local traits at crate::cpi:: level +pub use account::CpiAccountsTrait; +pub use instruction::LightCpiInstruction; +pub use invoke::{invoke_light_system_program, InvokeLightSystemProgram, LightInstructionData}; +// Re-export non-conflicting items from sdk-types +pub use light_sdk_types::{cpi_accounts::CpiAccountsConfig, CpiSigner}; + pub mod v1; #[cfg(feature = "v2")] pub mod v2; - -pub use account::*; -pub use instruction::*; -pub use invoke::InvokeLightSystemProgram; -pub use light_compressed_account::instruction_data::traits::LightInstructionData; -/// Derives cpi signer and bump to invoke the light system program at compile time. -pub use light_macros::derive_light_cpi_signer; -/// Contains program id, derived cpi signer, and bump, -pub use light_sdk_types::{cpi_accounts::CpiAccountsConfig, CpiSigner}; diff --git a/sdk-libs/sdk/src/error.rs b/sdk-libs/sdk/src/error.rs index bd542cc112..c9b7ec32c2 100644 --- a/sdk-libs/sdk/src/error.rs +++ b/sdk-libs/sdk/src/error.rs @@ -75,7 +75,7 @@ pub enum LightSdkError { InvalidCpiContextAccount, #[error("Invalid SolPool PDA account")] InvalidSolPoolPdaAccount, - #[error("CpigAccounts accounts slice starts with an invalid account. It should start with LightSystemProgram SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7.")] + #[error("CpiAccounts accounts slice starts with an invalid account. It should start with LightSystemProgram SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7.")] InvalidCpiAccountsOffset, #[error("Expected LightAccount to have no data for closure.")] ExpectedNoData, @@ -129,6 +129,34 @@ impl From for ProgramError { } } +/// Convert from SDK's LightSdkError to LightSdkTypesError. +/// This allows SDK error types to be used where types error types are expected +/// (e.g., in trait impls for LightPreInit, LightFinalize, AccountMetasVec). +impl From for LightSdkTypesError { + fn from(e: LightSdkError) -> Self { + match e { + LightSdkError::ConstraintViolation => LightSdkTypesError::ConstraintViolation, + LightSdkError::Borsh => LightSdkTypesError::Borsh, + LightSdkError::AccountError(e) => LightSdkTypesError::AccountError(e), + LightSdkError::Hasher(e) => LightSdkTypesError::Hasher(e), + LightSdkError::MissingCompressionInfo => LightSdkTypesError::MissingCompressionInfo, + LightSdkError::InvalidRentSponsor => LightSdkTypesError::InvalidRentSponsor, + LightSdkError::CpiAccountsIndexOutOfBounds(i) => { + LightSdkTypesError::CpiAccountsIndexOutOfBounds(i) + } + LightSdkError::ReadOnlyAccountsNotSupportedInCpiContext => { + LightSdkTypesError::ReadOnlyAccountsNotSupportedInCpiContext + } + LightSdkError::CompressedAccountError(e) => { + LightSdkTypesError::CompressedAccountError(e) + } + LightSdkError::ProgramError(e) => LightSdkTypesError::ProgramError(u64::from(e) as u32), + _ => LightSdkTypesError::ConstraintViolation, + } + } +} + +/// Convert from LightSdkTypesError to SDK's LightSdkError. impl From for LightSdkError { fn from(e: LightSdkTypesError) -> Self { match e { @@ -156,6 +184,26 @@ impl From for LightSdkError { LightSdkTypesError::InvalidCpiAccountsOffset => LightSdkError::InvalidCpiAccountsOffset, LightSdkTypesError::AccountError(e) => LightSdkError::AccountError(e), LightSdkTypesError::Hasher(e) => LightSdkError::Hasher(e), + LightSdkTypesError::ConstraintViolation => LightSdkError::ConstraintViolation, + LightSdkTypesError::Borsh => LightSdkError::Borsh, + LightSdkTypesError::MissingCompressionInfo => LightSdkError::MissingCompressionInfo, + LightSdkTypesError::InvalidRentSponsor => LightSdkError::InvalidRentSponsor, + LightSdkTypesError::BorshIo(_) => LightSdkError::Borsh, + LightSdkTypesError::ReadOnlyAccountsNotSupportedInCpiContext => { + LightSdkError::ReadOnlyAccountsNotSupportedInCpiContext + } + LightSdkTypesError::CompressedAccountError(e) => { + LightSdkError::CompressedAccountError(e) + } + LightSdkTypesError::AccountDataTooSmall + | LightSdkTypesError::InvalidInstructionData + | LightSdkTypesError::InvalidSeeds + | LightSdkTypesError::CpiFailed + | LightSdkTypesError::NotEnoughAccountKeys + | LightSdkTypesError::MissingRequiredSignature => LightSdkError::ConstraintViolation, + LightSdkTypesError::ProgramError(code) => { + LightSdkError::ProgramError(ProgramError::Custom(code)) + } } } } diff --git a/sdk-libs/sdk/src/instruction/mod.rs b/sdk-libs/sdk/src/instruction/mod.rs index 9574cccb22..95d0ce67d0 100644 --- a/sdk-libs/sdk/src/instruction/mod.rs +++ b/sdk-libs/sdk/src/instruction/mod.rs @@ -3,18 +3,18 @@ //! This module provides types and utilities for building Solana instructions that work with //! compressed accounts. The main workflow involves: //! ```text -//! ├─ 𝐂𝐥𝐢𝐞𝐧𝐭 -//! │ ├─ Get ValidityProof from RPC. -//! │ ├─ pack accounts with PackedAccounts into PackedAddressTreeInfo and PackedStateTreeInfo. -//! │ ├─ pack CompressedAccountMeta. -//! │ ├─ Build Instruction from packed accounts and CompressedAccountMetas. -//! │ └─ Send transaction -//! │ -//! └─ 𝐂𝐮𝐬𝐭𝐨𝐦 𝐏𝐫𝐨𝐠𝐫𝐚𝐦 -//! ├─ use PackedAddressTreeInfo to create a new address. -//! ├─ use CompressedAccountMeta to instantiate a LightAccount struct. -//! │ -//! └─ 𝐋𝐢𝐠𝐡𝐭 𝐒𝐲𝐬𝐭𝐞𝐦 𝐏𝐫𝐨𝐠𝐫𝐚𝐦 𝐂𝐏𝐈 +//! |- Client +//! | |- Get ValidityProof from RPC. +//! | |- pack accounts with PackedAccounts into PackedAddressTreeInfo and PackedStateTreeInfo. +//! | |- pack CompressedAccountMeta. +//! | |- Build Instruction from packed accounts and CompressedAccountMetas. +//! | |_ Send transaction +//! | +//! |_ Custom Program +//! |- use PackedAddressTreeInfo to create a new address. +//! |- use CompressedAccountMeta to instantiate a LightAccount struct. +//! | +//! |_ Light System Program CPI //! ``` //! ## Main Types //! @@ -40,44 +40,26 @@ // TODO: link to examples -// Only available off-chain (client-side) - contains sorting code that exceeds BPF stack limits -#[cfg(not(target_os = "solana"))] -mod pack_accounts; -mod system_accounts; -mod tree_info; - -// Stub type for on-chain compilation - allows trait signatures to compile -// The actual pack methods are never called on-chain -#[cfg(target_os = "solana")] -mod pack_accounts_stub { - use solana_pubkey::Pubkey; - - /// Stub type for on-chain compilation. The actual implementation with sorting - /// is only available off-chain. This allows trait signatures that reference - /// PackedAccounts to compile on Solana. - pub struct PackedAccounts { - _phantom: core::marker::PhantomData<()>, - } - - impl PackedAccounts { - pub fn insert_or_get(&mut self, _pubkey: Pubkey) -> u8 { - panic!("PackedAccounts::insert_or_get is not available on-chain") - } - - pub fn insert_or_get_read_only(&mut self, _pubkey: Pubkey) -> u8 { - panic!("PackedAccounts::insert_or_get_read_only is not available on-chain") - } - } -} - -/// Zero-knowledge proof to prove the validity of existing compressed accounts and new addresses. +// Re-export instruction types from sdk-types (available on all targets) +// SDK-specific: ValidityProof and CompressedProof pub use light_compressed_account::instruction_data::compressed_proof::{ CompressedProof, ValidityProof, }; pub use light_sdk_types::instruction::*; +// Re-export pack_accounts utilities (off-chain only, requires std for HashMap) #[cfg(not(target_os = "solana"))] -pub use pack_accounts::*; -#[cfg(target_os = "solana")] -pub use pack_accounts_stub::PackedAccounts; +pub use light_sdk_types::pack_accounts::*; + +// SDK-specific: system account helpers (depend on find_cpi_signer_macro!) +mod system_accounts; pub use system_accounts::*; + +// SDK-specific: tree info packing/unpacking +mod tree_info; pub use tree_info::*; + +// Newtype wrapper around generic PackedAccounts with inherent system account methods +#[cfg(not(target_os = "solana"))] +mod packed_accounts; +#[cfg(not(target_os = "solana"))] +pub use packed_accounts::PackedAccounts; diff --git a/sdk-libs/sdk/src/instruction/pack_accounts.rs b/sdk-libs/sdk/src/instruction/pack_accounts.rs deleted file mode 100644 index c6c5d4663c..0000000000 --- a/sdk-libs/sdk/src/instruction/pack_accounts.rs +++ /dev/null @@ -1,509 +0,0 @@ -//! Utilities for packing accounts into instruction data. -//! -//! [`PackedAccounts`] is a builder for efficiently organizing accounts into the three categories -//! required for compressed account instructions: -//! 1. **Pre-accounts** - Custom accounts needed before system accounts -//! 2. **System accounts** - Static light system program accounts -//! 3. **Packed accounts** - Dynamically packed accounts (Merkle trees, address trees, queues) with automatic deduplication -//! -//! -//! ## System Account Versioning -//! -//! **`add_system_accounts()` is complementary to [`cpi::v1::CpiAccounts`](crate::cpi::v1::CpiAccounts)** -//! **`add_system_accounts_v2()` is complementary to [`cpi::v2::CpiAccounts`](crate::cpi::v2::CpiAccounts)** -//! -//! Always use the matching version - v1 client-side account packing with v1 program-side CPI, -//! and v2 with v2. Mixing versions will cause account layout mismatches. -//! -//! # Example: Creating a compressed PDA -//! -//! ```rust -//! # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; -//! # use solana_pubkey::Pubkey; -//! # fn example() -> Result<(), Box> { -//! # let program_id = Pubkey::new_unique(); -//! # let payer_pubkey = Pubkey::new_unique(); -//! # let merkle_tree_pubkey = Pubkey::new_unique(); -//! // Initialize with system accounts -//! let system_account_meta_config = SystemAccountMetaConfig::new(program_id); -//! let mut accounts = PackedAccounts::default(); -//! -//! // Add pre-accounts (signers) -//! accounts.add_pre_accounts_signer(payer_pubkey); -//! -//! // Add Light system program accounts (v2) -//! #[cfg(feature = "v2")] -//! accounts.add_system_accounts_v2(system_account_meta_config)?; -//! #[cfg(not(feature = "v2"))] -//! accounts.add_system_accounts(system_account_meta_config)?; -//! -//! // Add Merkle tree accounts (automatically tracked and deduplicated) -//! let output_merkle_tree_index = accounts.insert_or_get(merkle_tree_pubkey); -//! -//! // Convert to final account metas with offsets -//! let (account_metas, system_accounts_offset, tree_accounts_offset) = accounts.to_account_metas(); -//! # assert_eq!(output_merkle_tree_index, 0); -//! # Ok(()) -//! # } -//! ``` -//! -//! # Account Organization -//! -//! The final account layout is: -//! ```text -//! [pre_accounts] [system_accounts] [packed_accounts] -//! ↑ ↑ ↑ -//! Signers, Light system Merkle trees, -//! fee payer program accts address trees -//! ``` -//! -//! # Automatic Deduplication -//! -//! ```rust -//! # use light_sdk::instruction::PackedAccounts; -//! # use solana_pubkey::Pubkey; -//! let mut accounts = PackedAccounts::default(); -//! let tree_pubkey = Pubkey::new_unique(); -//! let other_tree = Pubkey::new_unique(); -//! -//! // First insertion gets index 0 -//! let index1 = accounts.insert_or_get(tree_pubkey); -//! assert_eq!(index1, 0); -//! -//! // Same tree inserted again returns same index (deduplicated) -//! let index2 = accounts.insert_or_get(tree_pubkey); -//! assert_eq!(index2, 0); -//! -//! // Different tree gets next index -//! let index3 = accounts.insert_or_get(other_tree); -//! assert_eq!(index3, 1); -//! ``` -//! -//! # Building Instructions with Anchor Programs -//! -//! When building instructions for Anchor programs, concatenate your custom accounts with the packed accounts: -//! -//! ```rust,ignore -//! # use anchor_lang::InstructionData; -//! # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; -//! # use solana_instruction::{AccountMeta, Instruction}; -//! -//! // 1. Set up packed accounts -//! let config = SystemAccountMetaConfig::new(program_id); -//! let mut remaining_accounts = PackedAccounts::default(); -//! remaining_accounts.add_system_accounts(config)?; -//! -//! // 2. Pack tree accounts from proof result -//! let packed_tree_info = proof_result.pack_tree_infos(&mut remaining_accounts); -//! let output_tree_index = state_tree_info.pack_output_tree_index(&mut remaining_accounts)?; -//! -//! // 3. Convert to account metas -//! let (remaining_accounts, _, _) = remaining_accounts.to_account_metas(); -//! -//! // 4. Build instruction: custom accounts first, then remaining_accounts -//! let instruction = Instruction { -//! program_id: your_program::ID, -//! accounts: [ -//! vec![AccountMeta::new(payer.pubkey(), true)], // Your program's accounts -//! // Add other custom accounts here if needed -//! remaining_accounts, // Light system accounts + trees -//! ] -//! .concat(), -//! data: your_program::instruction::YourInstruction { -//! proof: proof_result.proof, -//! address_tree_info: packed_tree_info.address_trees[0], -//! output_tree_index, -//! // ... your other fields -//! } -//! .data(), -//! }; -//! ``` - -use std::collections::HashMap; - -use crate::{ - error::LightSdkError, - instruction::system_accounts::{get_light_system_account_metas, SystemAccountMetaConfig}, - AccountMeta, Pubkey, -}; - -/// Builder to collect accounts for compressed account instructions. -/// -/// Manages three categories of accounts: -/// - **Pre-accounts**: Signers and other custom accounts that come before system accounts. -/// - **System accounts**: Light system program accounts (authority, trees, queues). -/// - **Packed accounts**: Dynamically tracked deduplicted accounts. -/// -/// # Example -/// -/// ```rust -/// # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; -/// # use solana_pubkey::Pubkey; -/// # fn example() -> Result<(), Box> { -/// # let payer_pubkey = Pubkey::new_unique(); -/// # let program_id = Pubkey::new_unique(); -/// # let merkle_tree_pubkey = Pubkey::new_unique(); -/// let mut accounts = PackedAccounts::default(); -/// -/// // Add signer -/// accounts.add_pre_accounts_signer(payer_pubkey); -/// -/// // Add system accounts (use v2 if feature is enabled) -/// let config = SystemAccountMetaConfig::new(program_id); -/// #[cfg(feature = "v2")] -/// accounts.add_system_accounts_v2(config)?; -/// #[cfg(not(feature = "v2"))] -/// accounts.add_system_accounts(config)?; -/// -/// // Add and track tree accounts -/// let tree_index = accounts.insert_or_get(merkle_tree_pubkey); -/// -/// // Get final account metas -/// let (metas, system_offset, tree_offset) = accounts.to_account_metas(); -/// # assert_eq!(tree_index, 0); -/// # Ok(()) -/// # } -/// ``` -#[derive(Default, Debug)] -pub struct PackedAccounts { - /// Accounts that must come before system accounts (e.g., signers, fee payer). - pub pre_accounts: Vec, - /// Light system program accounts (authority, programs, trees, queues). - system_accounts: Vec, - /// Next available index for packed accounts. - next_index: u8, - /// Map of pubkey to (index, AccountMeta) for deduplication and index tracking. - map: HashMap, - /// Field to sanity check - system_accounts_set: bool, -} - -impl PackedAccounts { - pub fn new_with_system_accounts(config: SystemAccountMetaConfig) -> crate::error::Result { - let mut remaining_accounts = PackedAccounts::default(); - remaining_accounts.add_system_accounts(config)?; - Ok(remaining_accounts) - } - - pub fn system_accounts_set(&self) -> bool { - self.system_accounts_set - } - - pub fn add_pre_accounts_signer(&mut self, pubkey: Pubkey) { - self.pre_accounts.push(AccountMeta { - pubkey, - is_signer: true, - is_writable: false, - }); - } - - pub fn add_pre_accounts_signer_mut(&mut self, pubkey: Pubkey) { - self.pre_accounts.push(AccountMeta { - pubkey, - is_signer: true, - is_writable: true, - }); - } - - pub fn add_pre_accounts_meta(&mut self, account_meta: AccountMeta) { - self.pre_accounts.push(account_meta); - } - - pub fn add_pre_accounts_metas(&mut self, account_metas: &[AccountMeta]) { - self.pre_accounts.extend_from_slice(account_metas); - } - - /// Adds v1 Light system program accounts to the account list. - /// - /// **Use with [`cpi::v1::CpiAccounts`](crate::cpi::v1::CpiAccounts) on the program side.** - /// - /// This adds all the accounts required by the Light system program for v1 operations, - /// including the CPI authority, registered programs, account compression program, and Noop program. - /// - /// # Example - /// - /// ```rust - /// # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; - /// # use solana_pubkey::Pubkey; - /// # fn example() -> Result<(), Box> { - /// # let program_id = Pubkey::new_unique(); - /// let mut accounts = PackedAccounts::default(); - /// let config = SystemAccountMetaConfig::new(program_id); - /// accounts.add_system_accounts(config)?; - /// # Ok(()) - /// # } - /// ``` - pub fn add_system_accounts( - &mut self, - config: SystemAccountMetaConfig, - ) -> crate::error::Result<()> { - self.system_accounts - .extend(get_light_system_account_metas(config)); - // note cpi context account is part of the system accounts - /* if let Some(pubkey) = config.cpi_context { - if self.next_index != 0 { - return Err(crate::error::LightSdkError::CpiContextOrderingViolation); - } - self.insert_or_get(pubkey); - }*/ - Ok(()) - } - - /// Adds v2 Light system program accounts to the account list. - /// - /// **Use with [`cpi::v2::CpiAccounts`](crate::cpi::v2::CpiAccounts) on the program side.** - /// - /// This adds all the accounts required by the Light system program for v2 operations. - /// V2 uses a different account layout optimized for batched state trees. - /// - /// # Example - /// - /// ```rust - /// # #[cfg(feature = "v2")] - /// # { - /// # use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; - /// # use solana_pubkey::Pubkey; - /// # fn example() -> Result<(), Box> { - /// # let program_id = Pubkey::new_unique(); - /// let mut accounts = PackedAccounts::default(); - /// let config = SystemAccountMetaConfig::new(program_id); - /// accounts.add_system_accounts_v2(config)?; - /// # Ok(()) - /// # } - /// # } - /// ``` - #[cfg(feature = "v2")] - pub fn add_system_accounts_v2( - &mut self, - config: SystemAccountMetaConfig, - ) -> crate::error::Result<()> { - self.system_accounts - .extend(crate::instruction::get_light_system_account_metas_v2( - config, - )); - // note cpi context account is part of the system accounts - /* if let Some(pubkey) = config.cpi_context { - if self.next_index != 0 { - return Err(crate::error::LightSdkError::CpiContextOrderingViolation); - } - self.insert_or_get(pubkey); - }*/ - Ok(()) - } - - /// Returns the index of the provided `pubkey` in the collection. - /// - /// If the provided `pubkey` is not a part of the collection, it gets - /// inserted with a `next_index`. - /// - /// If the privided `pubkey` already exists in the collection, its already - /// existing index is returned. - pub fn insert_or_get(&mut self, pubkey: Pubkey) -> u8 { - self.insert_or_get_config(pubkey, false, true) - } - - pub fn insert_or_get_read_only(&mut self, pubkey: Pubkey) -> u8 { - self.insert_or_get_config(pubkey, false, false) - } - - pub fn insert_or_get_config( - &mut self, - pubkey: Pubkey, - is_signer: bool, - is_writable: bool, - ) -> u8 { - match self.map.get_mut(&pubkey) { - Some((index, entry)) => { - if !entry.is_writable { - entry.is_writable = is_writable; - } - if !entry.is_signer { - entry.is_signer = is_signer; - } - *index - } - None => { - let index = self.next_index; - self.next_index += 1; - self.map.insert( - pubkey, - ( - index, - AccountMeta { - pubkey, - is_signer, - is_writable, - }, - ), - ); - index - } - } - } - - fn hash_set_accounts_to_metas(&self) -> Vec { - let mut packed_accounts = self.map.iter().collect::>(); - // hash maps are not sorted so we need to sort manually and collect into a vector again - packed_accounts.sort_by(|a, b| a.1 .0.cmp(&b.1 .0)); - let packed_accounts = packed_accounts - .iter() - .map(|(_, (_, k))| k.clone()) - .collect::>(); - packed_accounts - } - - fn get_offsets(&self) -> (usize, usize) { - let system_accounts_start_offset = self.pre_accounts.len(); - let packed_accounts_start_offset = - system_accounts_start_offset + self.system_accounts.len(); - (system_accounts_start_offset, packed_accounts_start_offset) - } - - /// Converts the collection of accounts to a vector of - /// [`AccountMeta`](solana_instruction::AccountMeta), which can be used - /// as remaining accounts in instructions or CPI calls. - /// - /// # Returns - /// - /// A tuple of `(account_metas, system_accounts_offset, packed_accounts_offset)`: - /// - `account_metas`: All accounts concatenated in order: `[pre_accounts][system_accounts][packed_accounts]` - /// - `system_accounts_offset`: Index where system accounts start (= pre_accounts.len()) - /// - `packed_accounts_offset`: Index where packed accounts start (= pre_accounts.len() + system_accounts.len()) - /// - /// The `system_accounts_offset` can be used to slice the accounts when creating [`CpiAccounts`](crate::cpi::v1::CpiAccounts): - /// ```ignore - /// let accounts_for_cpi = &ctx.remaining_accounts[system_accounts_offset..]; - /// let cpi_accounts = CpiAccounts::new(fee_payer, accounts_for_cpi, cpi_signer)?; - /// ``` - /// - /// The offset can be hardcoded if your program always has the same pre-accounts layout, or passed - /// as a field in your instruction data. - pub fn to_account_metas(&self) -> (Vec, usize, usize) { - let packed_accounts = self.hash_set_accounts_to_metas(); - let (system_accounts_start_offset, packed_accounts_start_offset) = self.get_offsets(); - ( - [ - self.pre_accounts.clone(), - self.system_accounts.clone(), - packed_accounts, - ] - .concat(), - system_accounts_start_offset, - packed_accounts_start_offset, - ) - } - - pub fn packed_pubkeys(&self) -> Vec { - self.hash_set_accounts_to_metas() - .iter() - .map(|meta| meta.pubkey) - .collect() - } - - pub fn add_custom_system_accounts( - &mut self, - accounts: T, - ) -> crate::error::Result<()> { - accounts.get_account_metas_vec(self) - } -} - -pub trait AccountMetasVec { - fn get_account_metas_vec(&self, accounts: &mut PackedAccounts) -> Result<(), LightSdkError>; -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_remaining_accounts() { - let mut remaining_accounts = PackedAccounts::default(); - - let pubkey_1 = Pubkey::new_unique(); - let pubkey_2 = Pubkey::new_unique(); - let pubkey_3 = Pubkey::new_unique(); - let pubkey_4 = Pubkey::new_unique(); - - // Initial insertion. - assert_eq!(remaining_accounts.insert_or_get(pubkey_1), 0); - assert_eq!(remaining_accounts.insert_or_get(pubkey_2), 1); - assert_eq!(remaining_accounts.insert_or_get(pubkey_3), 2); - - assert_eq!( - remaining_accounts.to_account_metas().0.as_slice(), - &[ - AccountMeta { - pubkey: pubkey_1, - is_signer: false, - is_writable: true, - }, - AccountMeta { - pubkey: pubkey_2, - is_signer: false, - is_writable: true, - }, - AccountMeta { - pubkey: pubkey_3, - is_signer: false, - is_writable: true, - } - ] - ); - - // Insertion of already existing pubkeys. - assert_eq!(remaining_accounts.insert_or_get(pubkey_1), 0); - assert_eq!(remaining_accounts.insert_or_get(pubkey_2), 1); - assert_eq!(remaining_accounts.insert_or_get(pubkey_3), 2); - - assert_eq!( - remaining_accounts.to_account_metas().0.as_slice(), - &[ - AccountMeta { - pubkey: pubkey_1, - is_signer: false, - is_writable: true, - }, - AccountMeta { - pubkey: pubkey_2, - is_signer: false, - is_writable: true, - }, - AccountMeta { - pubkey: pubkey_3, - is_signer: false, - is_writable: true, - } - ] - ); - - // Again, initial insertion. - assert_eq!(remaining_accounts.insert_or_get(pubkey_4), 3); - - assert_eq!( - remaining_accounts.to_account_metas().0.as_slice(), - &[ - AccountMeta { - pubkey: pubkey_1, - is_signer: false, - is_writable: true, - }, - AccountMeta { - pubkey: pubkey_2, - is_signer: false, - is_writable: true, - }, - AccountMeta { - pubkey: pubkey_3, - is_signer: false, - is_writable: true, - }, - AccountMeta { - pubkey: pubkey_4, - is_signer: false, - is_writable: true, - } - ] - ); - } -} diff --git a/sdk-libs/sdk/src/instruction/packed_accounts.rs b/sdk-libs/sdk/src/instruction/packed_accounts.rs new file mode 100644 index 0000000000..f1a09537b8 --- /dev/null +++ b/sdk-libs/sdk/src/instruction/packed_accounts.rs @@ -0,0 +1,81 @@ +use std::ops::{Deref, DerefMut}; + +use super::system_accounts::{get_light_system_account_metas, SystemAccountMetaConfig}; + +type Inner = light_sdk_types::pack_accounts::PackedAccounts; + +/// Packs accounts and creates indices for instruction building (client-side). +/// +/// Wraps the generic `PackedAccounts` from sdk-types with +/// Solana-specific system account helpers as inherent methods. +#[derive(Debug, Default)] +pub struct PackedAccounts(pub Inner); + +impl Deref for PackedAccounts { + type Target = Inner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for PackedAccounts { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for PackedAccounts { + fn from(inner: Inner) -> Self { + Self(inner) + } +} + +impl From for Inner { + fn from(wrapper: PackedAccounts) -> Self { + wrapper.0 + } +} + +impl PackedAccounts { + /// Creates a new [`PackedAccounts`] with v1 system accounts pre-configured. + /// + /// **Use with [`cpi::v1::CpiAccounts`](crate::cpi::v1::CpiAccounts) on the program side.** + pub fn new_with_system_accounts(config: SystemAccountMetaConfig) -> crate::error::Result { + let mut accounts = Self::default(); + accounts.add_system_accounts(config)?; + Ok(accounts) + } + + /// Adds v1 Light system program accounts to the account list. + /// + /// **Use with [`cpi::v1::CpiAccounts`](crate::cpi::v1::CpiAccounts) on the program side.** + /// + /// This adds all the accounts required by the Light system program for v1 operations, + /// including the CPI authority, registered programs, account compression program, and Noop program. + pub fn add_system_accounts( + &mut self, + config: SystemAccountMetaConfig, + ) -> crate::error::Result<()> { + self.0 + .add_system_accounts_raw(get_light_system_account_metas(config)); + Ok(()) + } + + /// Adds v2 Light system program accounts to the account list. + /// + /// **Use with [`cpi::v2::CpiAccounts`](crate::cpi::v2::CpiAccounts) on the program side.** + /// + /// This adds all the accounts required by the Light system program for v2 operations. + /// V2 uses a different account layout optimized for batched state trees. + #[cfg(feature = "v2")] + pub fn add_system_accounts_v2( + &mut self, + config: SystemAccountMetaConfig, + ) -> crate::error::Result<()> { + self.0 + .add_system_accounts_raw(super::system_accounts::get_light_system_account_metas_v2( + config, + )); + Ok(()) + } +} diff --git a/sdk-libs/sdk/src/instruction/tree_info.rs b/sdk-libs/sdk/src/instruction/tree_info.rs index 0281d824df..cd91c585e0 100644 --- a/sdk-libs/sdk/src/instruction/tree_info.rs +++ b/sdk-libs/sdk/src/instruction/tree_info.rs @@ -1,6 +1,7 @@ pub use light_compressed_account::compressed_account::{MerkleContext, PackedMerkleContext}; use light_sdk_types::instruction::PackedAddressTreeInfo; +#[cfg(not(target_os = "solana"))] use super::PackedAccounts; use crate::{AccountInfo, AnchorDeserialize, AnchorSerialize, Pubkey}; @@ -10,6 +11,7 @@ pub struct AddressTreeInfo { pub queue: Pubkey, } +#[cfg(not(target_os = "solana"))] #[deprecated(since = "0.13.0", note = "please use PackedStateTreeInfo")] pub fn pack_merkle_contexts<'a, I>( merkle_contexts: I, @@ -22,6 +24,7 @@ where merkle_contexts.map(|x| pack_merkle_context(x, remaining_accounts)) } +#[cfg(not(target_os = "solana"))] #[deprecated(since = "0.13.0", note = "please use PackedStateTreeInfo")] pub fn pack_merkle_context( merkle_context: &MerkleContext, @@ -48,6 +51,7 @@ pub fn pack_merkle_context( /// Returns an iterator of [`PackedAddressTreeInfo`] and fills up /// `remaining_accounts` based on the given `merkle_contexts`. +#[cfg(not(target_os = "solana"))] pub fn pack_address_tree_infos<'a>( address_tree_infos: &'a [AddressTreeInfo], root_index: &'a [u16], @@ -63,6 +67,7 @@ pub fn pack_address_tree_infos<'a>( /// based on the given `merkle_context`. /// Packs Merkle tree account first. /// Packs queue account second. +#[cfg(not(target_os = "solana"))] pub fn pack_address_tree_info( address_tree_info: &AddressTreeInfo, remaining_accounts: &mut PackedAccounts, diff --git a/sdk-libs/sdk/src/interface/account/pack.rs b/sdk-libs/sdk/src/interface/account/pack.rs deleted file mode 100644 index 976e4a1d88..0000000000 --- a/sdk-libs/sdk/src/interface/account/pack.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Pack and Unpack traits for converting between full Pubkeys and u8 indices. - -use solana_account_info::AccountInfo; -use solana_program_error::ProgramError; - -#[cfg(not(target_os = "solana"))] -use crate::instruction::PackedAccounts; -#[cfg(not(target_os = "solana"))] -use crate::AnchorSerialize; - -/// Replace 32-byte Pubkeys with 1-byte indices to save space. -/// If your type has no Pubkeys, just return self. -#[cfg(not(target_os = "solana"))] -pub trait Pack { - type Packed: AnchorSerialize + Clone + std::fmt::Debug; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Result; -} - -pub trait Unpack { - type Unpacked; - - fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result; -} diff --git a/sdk-libs/sdk/src/interface/account/token_seeds.rs b/sdk-libs/sdk/src/interface/account/token_seeds.rs deleted file mode 100644 index 18d0c0a178..0000000000 --- a/sdk-libs/sdk/src/interface/account/token_seeds.rs +++ /dev/null @@ -1,260 +0,0 @@ -use light_compressed_account::compressed_account::PackedMerkleContext; -use light_sdk_types::instruction::PackedStateTreeInfo; -pub use light_token_interface::{ - instructions::{ - extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, - transfer2::MultiInputTokenDataWithContext, - }, - state::{ - extensions::{CompressedOnlyExtension, ExtensionStruct}, - AccountState, Token, TokenDataVersion, - }, -}; -use solana_account_info::AccountInfo; -use solana_program_error::ProgramError; -use solana_pubkey::Pubkey; - -use super::pack::Unpack; -// Pack trait and PackedAccounts only available off-chain (client-side packing) -#[cfg(not(target_os = "solana"))] -use crate::{instruction::PackedAccounts, interface::Pack}; -use crate::{ - interface::{ - AccountType, LightAccountVariantTrait, PackedLightAccountVariantTrait, PackedTokenSeeds, - UnpackedTokenSeeds, - }, - AnchorDeserialize, AnchorSerialize, -}; - -#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] -pub struct TokenDataWithSeeds { - pub seeds: S, - pub token_data: Token, -} -#[repr(C)] -#[derive(Debug, Copy, Clone, Default, PartialEq, AnchorSerialize, AnchorDeserialize)] -pub struct PackedTokenData { - pub owner: u8, - pub amount: u64, - pub has_delegate: bool, // Optional delegate is set - pub delegate: u8, - pub mint: u8, - pub version: u8, -} - -#[derive(Debug, AnchorSerialize, AnchorDeserialize, Clone)] -pub struct TokenDataWithPackedSeeds< - S: Unpack + AnchorSerialize + AnchorDeserialize + Clone + std::fmt::Debug, -> { - pub seeds: S, - pub token_data: PackedTokenData, - pub extension: Option, -} - -#[cfg(not(target_os = "solana"))] -impl Pack for TokenDataWithSeeds -where - S: Pack, - S::Packed: Unpack + AnchorDeserialize + AnchorSerialize + Clone + std::fmt::Debug, -{ - type Packed = TokenDataWithPackedSeeds; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Result { - let seeds = self.seeds.pack(remaining_accounts)?; - - let owner_index = remaining_accounts - .insert_or_get(Pubkey::new_from_array(self.token_data.owner.to_bytes())); - - let token_data = PackedTokenData { - owner: owner_index, - amount: self.token_data.amount, - has_delegate: self.token_data.delegate.is_some(), - delegate: self - .token_data - .delegate - .map(|d| remaining_accounts.insert_or_get(Pubkey::new_from_array(d.to_bytes()))) - .unwrap_or(0), - mint: remaining_accounts - .insert_or_get(Pubkey::new_from_array(self.token_data.mint.to_bytes())), - version: TokenDataVersion::ShaFlat as u8, - }; - - // Extract CompressedOnly extension from Token state if present. - let extension = self.token_data.extensions.as_ref().and_then(|exts| { - exts.iter().find_map(|ext| { - if let ExtensionStruct::CompressedOnly(co) = ext { - Some(CompressedOnlyExtensionInstructionData { - delegated_amount: co.delegated_amount, - withheld_transfer_fee: co.withheld_transfer_fee, - is_frozen: self.token_data.state == AccountState::Frozen, - compression_index: 0, - is_ata: co.is_ata != 0, - bump: 0, - owner_index, - }) - } else { - None - } - }) - }); - - Ok(TokenDataWithPackedSeeds { - seeds, - token_data, - extension, - }) - } -} - -impl Unpack for TokenDataWithPackedSeeds -where - S: Unpack + AnchorSerialize + AnchorDeserialize + Clone + std::fmt::Debug, -{ - type Unpacked = TokenDataWithSeeds; - - fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result { - let seeds = self.seeds.unpack(remaining_accounts)?; - - let owner_key = remaining_accounts - .get(self.token_data.owner as usize) - .ok_or(ProgramError::InvalidAccountData)? - .key; - let mint_key = remaining_accounts - .get(self.token_data.mint as usize) - .ok_or(ProgramError::InvalidAccountData)? - .key; - let delegate = if self.token_data.has_delegate { - let delegate_key = remaining_accounts - .get(self.token_data.delegate as usize) - .ok_or(ProgramError::InvalidAccountData)? - .key; - Some(light_compressed_account::Pubkey::from( - delegate_key.to_bytes(), - )) - } else { - None - }; - - // Reconstruct extensions from instruction extension data. - let extensions = self.extension.map(|ext| { - vec![ExtensionStruct::CompressedOnly(CompressedOnlyExtension { - delegated_amount: ext.delegated_amount, - withheld_transfer_fee: ext.withheld_transfer_fee, - is_ata: ext.is_ata as u8, - })] - }); - - let state = self.extension.map_or(AccountState::Initialized, |ext| { - if ext.is_frozen { - AccountState::Frozen - } else { - AccountState::Initialized - } - }); - - let delegated_amount = self.extension.map_or(0, |ext| ext.delegated_amount); - - let token_data = Token { - mint: light_compressed_account::Pubkey::from(mint_key.to_bytes()), - owner: light_compressed_account::Pubkey::from(owner_key.to_bytes()), - amount: self.token_data.amount, - delegate, - state, - is_native: None, - delegated_amount, - close_authority: None, - account_type: TokenDataVersion::ShaFlat as u8, - extensions, - }; - - Ok(TokenDataWithSeeds { seeds, token_data }) - } -} - -// ============================================================================= -// Blanket impls: LightAccountVariantTrait / PackedLightAccountVariantTrait -// for TokenDataWithSeeds / TokenDataWithPackedSeeds -// where S implements the seed-specific helper traits. -// ============================================================================= - -impl LightAccountVariantTrait for TokenDataWithSeeds -where - S: UnpackedTokenSeeds, - S::Packed: PackedTokenSeeds + Unpack, -{ - const PROGRAM_ID: Pubkey = S::PROGRAM_ID; - type Seeds = S; - type Data = Token; - type Packed = TokenDataWithPackedSeeds; - - fn data(&self) -> &Self::Data { - &self.token_data - } - - fn seed_vec(&self) -> Vec> { - self.seeds.seed_vec() - } - - fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; N] { - self.seeds.seed_refs_with_bump(bump_storage) - } -} - -impl PackedLightAccountVariantTrait for TokenDataWithPackedSeeds -where - S: PackedTokenSeeds, - S::Unpacked: UnpackedTokenSeeds, -{ - type Unpacked = TokenDataWithSeeds; - - const ACCOUNT_TYPE: AccountType = AccountType::Token; - - fn bump(&self) -> u8 { - self.seeds.bump() - } - - fn unpack(&self, accounts: &[AccountInfo]) -> anchor_lang::Result { - ::unpack(self, accounts).map_err(anchor_lang::error::Error::from) - } - - fn seed_refs_with_bump<'a>( - &'a self, - accounts: &'a [AccountInfo], - bump_storage: &'a [u8; 1], - ) -> std::result::Result<[&'a [u8]; N], ProgramError> { - self.seeds.seed_refs_with_bump(accounts, bump_storage) - } - - fn into_in_token_data( - &self, - tree_info: &PackedStateTreeInfo, - output_queue_index: u8, - ) -> anchor_lang::Result { - Ok(MultiInputTokenDataWithContext { - amount: self.token_data.amount, - mint: self.token_data.mint, - owner: self.token_data.owner, - version: self.token_data.version, - has_delegate: self.token_data.has_delegate, - delegate: self.token_data.delegate, - root_index: tree_info.root_index, - merkle_context: PackedMerkleContext { - merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, - queue_pubkey_index: output_queue_index, - leaf_index: tree_info.leaf_index, - prove_by_index: tree_info.prove_by_index, - }, - }) - } - - fn into_in_tlv(&self) -> anchor_lang::Result>> { - Ok(self - .extension - .as_ref() - .map(|ext| vec![ExtensionInstructionData::CompressedOnly(*ext)])) - } - - fn derive_owner(&self) -> Pubkey { - self.seeds.derive_owner() - } -} diff --git a/sdk-libs/sdk/src/interface/accounts/create_pda.rs b/sdk-libs/sdk/src/interface/accounts/create_pda.rs deleted file mode 100644 index 92ab4e505f..0000000000 --- a/sdk-libs/sdk/src/interface/accounts/create_pda.rs +++ /dev/null @@ -1,126 +0,0 @@ -use solana_account_info::AccountInfo; -use solana_cpi::invoke_signed; -use solana_pubkey::Pubkey; -use solana_system_interface::instruction as system_instruction; - -use crate::error::LightSdkError; - -/// Cold path: Account already has lamports (e.g., attacker donation). -/// Uses Assign + Allocate + Transfer instead of CreateAccount which would fail. -#[cold] -#[allow(clippy::too_many_arguments)] -fn create_pda_account_with_lamports<'info>( - rent_sponsor: &AccountInfo<'info>, - rent_sponsor_seeds: &[&[u8]], - solana_account: &AccountInfo<'info>, - lamports: u64, - space: u64, - owner: &Pubkey, - seeds: &[&[u8]], - system_program: &AccountInfo<'info>, -) -> Result<(), LightSdkError> { - let current_lamports = solana_account.lamports(); - - // Assign owner - let assign_ix = system_instruction::assign(solana_account.key, owner); - invoke_signed( - &assign_ix, - &[solana_account.clone(), system_program.clone()], - &[seeds], - ) - .map_err(LightSdkError::ProgramError)?; - - // Allocate space - let allocate_ix = system_instruction::allocate(solana_account.key, space); - invoke_signed( - &allocate_ix, - &[solana_account.clone(), system_program.clone()], - &[seeds], - ) - .map_err(LightSdkError::ProgramError)?; - - // Transfer remaining lamports for rent-exemption if needed - if lamports > current_lamports { - let transfer_ix = system_instruction::transfer( - rent_sponsor.key, - solana_account.key, - lamports - current_lamports, - ); - // Include rent sponsor seeds so the PDA can sign for the transfer - invoke_signed( - &transfer_ix, - &[ - rent_sponsor.clone(), - solana_account.clone(), - system_program.clone(), - ], - &[rent_sponsor_seeds], - ) - .map_err(LightSdkError::ProgramError)?; - } - - Ok(()) -} - -/// Creates a PDA account, handling the case where the account already has lamports. -/// -/// This function handles the edge case where an attacker might have donated lamports -/// to the PDA address before decompression. In that case, `CreateAccount` would fail, -/// so we fall back to `Assign + Allocate + Transfer`. -/// -/// # Arguments -/// * `rent_sponsor` - Account paying for rent (must be a PDA derived from the calling program) -/// * `rent_sponsor_seeds` - Seeds for the rent sponsor PDA (including bump) for signing -/// * `solana_account` - The PDA account to create -/// * `lamports` - Amount of lamports for rent-exemption -/// * `space` - Size of the account in bytes -/// * `owner` - Program that will own the account -/// * `seeds` - Seeds for the target PDA (including bump) for signing -/// * `system_program` - System program -#[inline(never)] -#[allow(clippy::too_many_arguments)] -pub fn create_pda_account<'info>( - rent_sponsor: &AccountInfo<'info>, - rent_sponsor_seeds: &[&[u8]], - solana_account: &AccountInfo<'info>, - lamports: u64, - space: u64, - owner: &Pubkey, - seeds: &[&[u8]], - system_program: &AccountInfo<'info>, -) -> Result<(), LightSdkError> { - // Cold path: account already has lamports (e.g., attacker donation) - if solana_account.lamports() > 0 { - return create_pda_account_with_lamports( - rent_sponsor, - rent_sponsor_seeds, - solana_account, - lamports, - space, - owner, - seeds, - system_program, - ); - } - - // Normal path: CreateAccount - // Include both rent sponsor seeds (payer) and PDA seeds (new account) - let create_account_ix = system_instruction::create_account( - rent_sponsor.key, - solana_account.key, - lamports, - space, - owner, - ); - - invoke_signed( - &create_account_ix, - &[ - rent_sponsor.clone(), - solana_account.clone(), - system_program.clone(), - ], - &[rent_sponsor_seeds, seeds], - ) - .map_err(LightSdkError::ProgramError) -} diff --git a/sdk-libs/sdk/src/interface/accounts/init_compressed_account.rs b/sdk-libs/sdk/src/interface/accounts/init_compressed_account.rs deleted file mode 100644 index 11811cba63..0000000000 --- a/sdk-libs/sdk/src/interface/accounts/init_compressed_account.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! Helper functions for preparing compressed accounts on init. - -use light_compressed_account::{ - address::derive_address, - instruction_data::{data::NewAddressParamsAssignedPacked, with_account_info::OutAccountInfo}, -}; -use light_compressible::DECOMPRESSED_PDA_DISCRIMINATOR; -use light_hasher::{errors::HasherError, sha256::Sha256BE, Hasher}; -use light_sdk_types::constants::RENT_SPONSOR_SEED; -use solana_account_info::AccountInfo; -use solana_program_error::ProgramError; -use solana_pubkey::Pubkey; -use solana_sysvar::{rent::Rent, Sysvar}; - -use crate::{ - compressed_account::CompressedAccountInfo, error::LightSdkError, - instruction::PackedAddressTreeInfo, light_account_checks::checks::check_mut, -}; - -/// Prepare a compressed account for a PDA during initialization. -/// -/// This function handles the common pattern of: -/// 1. Deriving the compressed address from the PDA pubkey seed -/// 2. Creating NewAddressParamsAssignedPacked for the address tree -/// 3. Building CompressedAccountInfo with hashed PDA pubkey data -/// -/// Uses: -/// - Discriminator: `[255, 255, 255, 255, 255, 255, 255, 0]` - marks this as a -/// rent-free PDA placeholder (distinct from actual account data discriminators) -/// - Data: PDA pubkey bytes (32 bytes) - allows lookup/verification of the -/// compressed account by its on-chain PDA address -/// -/// # Arguments -/// * `pda_pubkey` - The PDA's pubkey (used as address seed and data) -/// * `address_tree_pubkey` - The address Merkle tree pubkey -/// * `address_tree_info` - Packed address tree info from CreateAccountsProof -/// * `output_tree_index` - Output state tree index -/// * `assigned_account_index` - Index in the accounts array (for assigned_account_index) -/// * `program_id` - The program ID (owner of the compressed account) -/// * `new_address_params` - Vector to push new address params into -/// * `account_infos` - Vector to push compressed account info into -#[inline(never)] -#[allow(clippy::too_many_arguments)] -pub fn prepare_compressed_account_on_init( - pda_pubkey: &Pubkey, - address_tree_pubkey: &Pubkey, - address_tree_info: &PackedAddressTreeInfo, - output_tree_index: u8, - assigned_account_index: u8, - program_id: &Pubkey, - new_address_params: &mut Vec, - account_infos: &mut Vec, -) -> Result<(), HasherError> { - // Data is always the PDA pubkey bytes - let data = pda_pubkey.to_bytes().to_vec(); - - // Derive compressed address from PDA pubkey seed - let address_seed = pda_pubkey.to_bytes(); - let address = derive_address( - &address_seed, - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - // Create and push new address params - new_address_params.push(NewAddressParamsAssignedPacked { - seed: address_seed, - address_merkle_tree_account_index: address_tree_info.address_merkle_tree_pubkey_index, - address_queue_account_index: address_tree_info.address_queue_pubkey_index, - address_merkle_tree_root_index: address_tree_info.root_index, - assigned_to_account: true, - assigned_account_index, - }); - - // Hash the data for the compressed account - let data_hash = Sha256BE::hash(&data)?; - - // Create and push CompressedAccountInfo - account_infos.push(CompressedAccountInfo { - address: Some(address), - input: None, - output: Some(OutAccountInfo { - discriminator: DECOMPRESSED_PDA_DISCRIMINATOR, - output_merkle_tree_index: output_tree_index, - lamports: 0, - data, - data_hash, - }), - }); - - Ok(()) -} - -/// Safe variant that validates PDA derivation before preparing compressed account. -/// -/// # Arguments -/// * `pda_pubkey` - The PDA's pubkey (used as address seed and data) -/// * `pda_seeds` - Seeds used to derive the PDA (without bump) -/// * `pda_bump` - The bump seed for the PDA -/// * `address_tree_pubkey` - The address Merkle tree pubkey -/// * `address_tree_info` - Packed address tree info from CreateAccountsProof -/// * `output_tree_index` - Output state tree index -/// * `assigned_account_index` - Index in the accounts array -/// * `program_id` - The program ID (owner of the compressed account) -/// * `new_address_params` - Vector to push new address params into -/// * `account_infos` - Vector to push compressed account info into -#[inline(never)] -#[allow(clippy::too_many_arguments)] -pub fn prepare_compressed_account_on_init_checked( - pda_pubkey: &Pubkey, - pda_seeds: &[&[u8]], - pda_bump: u8, - address_tree_pubkey: &Pubkey, - address_tree_info: &PackedAddressTreeInfo, - output_tree_index: u8, - assigned_account_index: u8, - program_id: &Pubkey, - new_address_params: &mut Vec, - account_infos: &mut Vec, -) -> Result<(), ProgramError> { - // Validate PDA derivation - let bump_slice = [pda_bump]; - let seeds_with_bump: Vec<&[u8]> = pda_seeds - .iter() - .copied() - .chain(std::iter::once(bump_slice.as_slice())) - .collect(); - - let expected_pda = Pubkey::create_program_address(&seeds_with_bump, program_id) - .map_err(|_| ProgramError::InvalidSeeds)?; - - if pda_pubkey != &expected_pda { - solana_msg::msg!( - "PDA key mismatch: expected {:?}, got {:?}", - expected_pda, - pda_pubkey - ); - return Err(ProgramError::InvalidSeeds); - } - - prepare_compressed_account_on_init( - pda_pubkey, - address_tree_pubkey, - address_tree_info, - output_tree_index, - assigned_account_index, - program_id, - new_address_params, - account_infos, - ) - .map_err(|e| LightSdkError::from(e).into()) -} - -/// Reimburse the fee payer for rent paid during PDA initialization. -/// -/// When using Anchor's `#[account(init)]` with `#[light_account(init)]`, the fee_payer -/// pays for rent-exemption. Since these become rent-free compressed accounts, this function -/// transfers the total rent amount back to the fee_payer from the program's rent sponsor PDA. -/// -/// # Arguments -/// * `created_accounts` - Slice of AccountInfo for the PDAs that were created -/// * `fee_payer` - The account that paid for rent (will receive reimbursement) -/// * `rent_sponsor` - The program's rent sponsor PDA (must be mutable, pays reimbursement) -/// * `program_id` - The program ID (for deriving rent sponsor PDA bump) -/// -/// # Seeds -/// The rent sponsor PDA is derived using: `[RENT_SPONSOR_SEED]` -pub fn reimburse_rent<'info>( - created_accounts: &[AccountInfo<'info>], - fee_payer: &AccountInfo<'info>, - rent_sponsor: &AccountInfo<'info>, - program_id: &Pubkey, -) -> Result<(), ProgramError> { - if created_accounts.is_empty() { - return Ok(()); - } - - // Calculate total rent-exemption for all created accounts - let rent = Rent::get()?; - let total_lamports: u64 = created_accounts - .iter() - .map(|acc| rent.minimum_balance(acc.data_len())) - .sum(); - - if total_lamports == 0 { - return Ok(()); - } - - // Derive rent sponsor bump - let (expected_rent_sponsor, rent_sponsor_bump) = - Pubkey::find_program_address(&[RENT_SPONSOR_SEED], program_id); - - // Verify the rent sponsor account matches expected PDA - if rent_sponsor.key != &expected_rent_sponsor { - solana_msg::msg!( - "rent_sponsor mismatch: expected {:?}, got {:?}", - expected_rent_sponsor, - rent_sponsor.key - ); - return Err(LightSdkError::InvalidRentSponsor.into()); - } - - // Validate accounts are writable for transfer - check_mut(rent_sponsor).map_err(ProgramError::from)?; - check_mut(fee_payer).map_err(ProgramError::from)?; - - // Transfer from rent sponsor to fee payer - let transfer_ix = solana_system_interface::instruction::transfer( - rent_sponsor.key, - fee_payer.key, - total_lamports, - ); - - let bump_bytes = [rent_sponsor_bump]; - let rent_sponsor_seeds: &[&[u8]] = &[RENT_SPONSOR_SEED, &bump_bytes]; - - solana_cpi::invoke_signed( - &transfer_ix, - &[rent_sponsor.clone(), fee_payer.clone()], - &[rent_sponsor_seeds], - )?; - - Ok(()) -} diff --git a/sdk-libs/sdk/src/interface/mod.rs b/sdk-libs/sdk/src/interface/mod.rs deleted file mode 100644 index 25d379d018..0000000000 --- a/sdk-libs/sdk/src/interface/mod.rs +++ /dev/null @@ -1,137 +0,0 @@ -//! Light Protocol interface module. -//! -//! This module provides the interface for compressible accounts, organized by -//! macro hierarchy: -//! -//! - `program/` - #[light_program] level (instruction processors) -//! - `accounts/` - #[derive(LightAccounts)] level (context structs, validation) -//! - `account/` - #[derive(LightAccount)] level (single account operations) - -// --- Subdirectory modules --- -pub mod account; -pub mod accounts; -pub mod program; - -// ============================================================================= -// BACKWARD COMPATIBILITY: Submodule path preservation -// ============================================================================= -// External code uses paths like `light_sdk::interface::config::LightConfig` -// and `light_sdk::interface::token::*`. Preserve with re-export aliases. - -/// Re-export config module for backward compatibility. -pub mod config { - pub use super::program::config::*; -} - -/// Re-export validation module for backward compatibility. -pub mod validation { - pub use super::program::validation::*; -} - -/// Re-export token module for backward compatibility. -#[cfg(feature = "anchor")] -pub mod token { - pub use super::{ - account::token_seeds::*, - program::decompression::token::prepare_token_account_for_decompression, - }; -} - -/// Re-export compression_info module for backward compatibility. -pub mod compression_info { - pub use super::account::compression_info::*; -} - -/// Re-export close module for backward compatibility. -#[cfg(feature = "v2")] -pub mod close { - pub use super::program::compression::close::*; -} - -/// Re-export finalize module for backward compatibility. -pub mod finalize { - pub use super::accounts::finalize::*; -} - -/// Re-export traits module for backward compatibility. -pub mod traits { - #[cfg(feature = "anchor")] - pub use super::account::light_account::{AccountType, LightAccount}; - pub use super::program::variant::IntoVariant; - #[cfg(feature = "anchor")] - pub use super::program::variant::{ - LightAccountVariantTrait, PackedLightAccountVariantTrait, PackedTokenSeeds, - UnpackedTokenSeeds, - }; -} - -// ============================================================================= -// BACKWARD COMPATIBILITY: Flat re-exports at interface level -// ============================================================================= -// The root interface/mod.rs re-exports everything at the flat level for -// backward compatibility with existing code. - -// --- Re-exports from program/ --- -// --- Re-exports from account/ --- -// Pack trait is only available off-chain (client-side) - uses PackedAccounts -#[cfg(feature = "anchor")] -pub use account::light_account::{AccountType, LightAccount}; -#[cfg(not(target_os = "solana"))] -pub use account::pack::Pack; -// --- Re-exports from program/variant --- -#[cfg(all(feature = "v2", feature = "cpi-context"))] -pub use account::pda_seeds::{HasTokenVariant, PdaSeedDerivation}; -pub use account::{ - compression_info::{ - claim_completed_epoch_rent, CompressAs, CompressedAccountData, CompressedInitSpace, - CompressionInfo, CompressionInfoField, CompressionState, HasCompressionInfo, Space, - COMPRESSION_INFO_SIZE, OPTION_COMPRESSION_INFO_SPACE, - }, - pack::Unpack, -}; -// --- Re-exports from accounts/ --- -#[cfg(feature = "v2")] -pub use accounts::create_pda::create_pda_account; -pub use accounts::{ - finalize::{LightFinalize, LightPreInit}, - init_compressed_account::{ - prepare_compressed_account_on_init, prepare_compressed_account_on_init_checked, - reimburse_rent, - }, -}; -// --- Re-exports from external crates --- -pub use light_compressible::{rent, CreateAccountsProof}; -#[cfg(feature = "v2")] -pub use program::compression::close::close; -#[cfg(feature = "anchor")] -pub use program::compression::pda::prepare_account_for_compression; -#[cfg(feature = "anchor")] -pub use program::compression::processor::process_compress_pda_accounts_idempotent; -#[cfg(feature = "anchor")] -pub use program::compression::processor::{ - CompressAndCloseParams, CompressCtx, CompressDispatchFn, -}; -#[cfg(feature = "anchor")] -pub use program::decompression::pda::prepare_account_for_decompression; -#[cfg(feature = "anchor")] -pub use program::decompression::processor::{ - process_decompress_pda_accounts_idempotent, DecompressCtx, DecompressIdempotentParams, - DecompressVariant, -}; -#[cfg(feature = "anchor")] -pub use program::variant::{ - LightAccountVariantTrait, PackedLightAccountVariantTrait, PackedTokenSeeds, UnpackedTokenSeeds, -}; -pub use program::{ - config::{ - process_initialize_light_config, process_initialize_light_config_checked, - process_update_light_config, LightConfig, COMPRESSIBLE_CONFIG_SEED, - MAX_ADDRESS_TREES_PER_SPACE, - }, - validation::{ - extract_tail_accounts, is_pda_initialized, should_skip_compression, - split_at_system_accounts_offset, validate_compress_accounts, validate_decompress_accounts, - ValidatedPdaContext, - }, - variant::IntoVariant, -}; diff --git a/sdk-libs/sdk/src/interface/program/compression/close.rs b/sdk-libs/sdk/src/interface/program/compression/close.rs deleted file mode 100644 index d19d8390b8..0000000000 --- a/sdk-libs/sdk/src/interface/program/compression/close.rs +++ /dev/null @@ -1,45 +0,0 @@ -use solana_account_info::AccountInfo; - -use crate::error::{LightSdkError, Result}; - -// close native solana account -pub fn close<'info>( - info: &mut AccountInfo<'info>, - sol_destination: &AccountInfo<'info>, -) -> Result<()> { - let system_program_id = solana_pubkey::pubkey!("11111111111111111111111111111111"); - - if info.key == sol_destination.key { - info.assign(&system_program_id); - info.resize(0) - .map_err(|_| LightSdkError::ConstraintViolation)?; - return Ok(()); - } - - let lamports_to_transfer = info.lamports(); - - let new_destination_lamports = sol_destination - .lamports() - .checked_add(lamports_to_transfer) - .ok_or(LightSdkError::ConstraintViolation)?; - - { - let mut destination_lamports = sol_destination - .try_borrow_mut_lamports() - .map_err(|_| LightSdkError::ConstraintViolation)?; - **destination_lamports = new_destination_lamports; - } - - { - let mut source_lamports = info - .try_borrow_mut_lamports() - .map_err(|_| LightSdkError::ConstraintViolation)?; - **source_lamports = 0; - } - - info.assign(&system_program_id); - info.resize(0) - .map_err(|_| LightSdkError::ConstraintViolation)?; - - Ok(()) -} diff --git a/sdk-libs/sdk/src/interface/program/compression/mod.rs b/sdk-libs/sdk/src/interface/program/compression/mod.rs deleted file mode 100644 index d308bd5984..0000000000 --- a/sdk-libs/sdk/src/interface/program/compression/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -//! Compression functions for PDA accounts. - -#[cfg(feature = "v2")] -pub mod close; - -#[cfg(feature = "anchor")] -pub mod processor; - -#[cfg(feature = "anchor")] -pub mod pda; diff --git a/sdk-libs/sdk/src/interface/program/compression/processor.rs b/sdk-libs/sdk/src/interface/program/compression/processor.rs deleted file mode 100644 index 7cfb98dd14..0000000000 --- a/sdk-libs/sdk/src/interface/program/compression/processor.rs +++ /dev/null @@ -1,158 +0,0 @@ -//! Compression instruction processor. - -use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo; -use light_sdk_types::{ - instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, CpiSigner, -}; -use solana_account_info::AccountInfo; -use solana_program_error::ProgramError; -use solana_pubkey::Pubkey; - -use crate::{ - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - instruction::ValidityProof, - interface::LightConfig, - AnchorDeserialize, AnchorSerialize, -}; - -/// Parameters for compress_and_close instruction. -/// Matches SDK's SaveAccountsData field order for compatibility. -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct CompressAndCloseParams { - /// Validity proof for compressed account verification - pub proof: ValidityProof, - /// Accounts to compress (meta only - data read from PDA) - pub compressed_accounts: Vec, - /// Offset into remaining_accounts where Light system accounts begin - pub system_accounts_offset: u8, -} - -/// Context struct holding all data needed for compression. -/// Contains internal vec for collecting CompressedAccountInfo results. -pub struct CompressCtx<'a, 'info> { - pub program_id: &'a Pubkey, - pub cpi_accounts: &'a CpiAccounts<'a, 'info>, - pub remaining_accounts: &'a [AccountInfo<'info>], - pub rent_sponsor: &'a AccountInfo<'info>, - pub light_config: &'a LightConfig, - /// Internal vec - dispatch functions push results here - pub compressed_account_infos: Vec, - /// Track which PDA indices to close - pub pda_indices_to_close: Vec, - /// Set to true if any account is not yet compressible. - /// When set, the entire batch is skipped (no CPI, no closes). - pub has_non_compressible: bool, -} - -/// Callback type for discriminator-based dispatch. -/// MACRO-GENERATED: Just a match statement routing to prepare_account_for_compression. -/// Takes &mut CompressCtx and pushes CompressedAccountInfo into ctx.compressed_account_infos. -/// -/// The dispatch function is responsible for: -/// 1. Reading the discriminator from the account data -/// 2. Deserializing the account based on discriminator -/// 3. Calling prepare_account_for_compression with the deserialized data -pub type CompressDispatchFn<'info> = fn( - account_info: &AccountInfo<'info>, - compressed_account_meta: &CompressedAccountMetaNoLamportsNoAddress, - index: usize, - ctx: &mut CompressCtx<'_, 'info>, -) -> std::result::Result<(), ProgramError>; - -/// Remaining accounts layout: -/// [0]: fee_payer (Signer, mut) -/// [1]: config (LightConfig PDA) -/// [2]: rent_sponsor (mut) -/// [3]: compression_authority (Signer) -/// [system_accounts_offset..]: Light system accounts for CPI -/// [remaining_accounts.len() - num_pda_accounts..]: PDA accounts to compress -/// -/// Runtime processor - handles all the plumbing, delegates dispatch to callback. -/// -/// **Takes raw instruction data** and deserializes internally - minimizes macro code. -/// **Uses only remaining_accounts** - no Context struct needed. -pub fn process_compress_pda_accounts_idempotent<'info>( - remaining_accounts: &[AccountInfo<'info>], - instruction_data: &[u8], - dispatch_fn: CompressDispatchFn<'info>, - cpi_signer: CpiSigner, - program_id: &Pubkey, -) -> std::result::Result<(), ProgramError> { - // Deserialize params internally - let params = CompressAndCloseParams::try_from_slice(instruction_data).map_err(|e| { - solana_msg::msg!("compress: params deser failed: {:?}", e); - ProgramError::InvalidInstructionData - })?; - - // Extract and validate accounts using shared validation - let validated_ctx = - crate::interface::validation::validate_compress_accounts(remaining_accounts, program_id)?; - let fee_payer = &validated_ctx.fee_payer; - let rent_sponsor = &validated_ctx.rent_sponsor; - let light_config = validated_ctx.light_config; - - let (_, system_accounts) = crate::interface::validation::split_at_system_accounts_offset( - remaining_accounts, - params.system_accounts_offset, - )?; - - let cpi_accounts = CpiAccounts::new(fee_payer, system_accounts, cpi_signer); - - // Build context struct with all needed data (includes internal vec) - let mut compress_ctx = CompressCtx { - program_id, - cpi_accounts: &cpi_accounts, - remaining_accounts, - rent_sponsor, - light_config: &light_config, - compressed_account_infos: Vec::with_capacity(params.compressed_accounts.len()), - pda_indices_to_close: Vec::with_capacity(params.compressed_accounts.len()), - has_non_compressible: false, - }; - - // PDA accounts at end of remaining_accounts - let pda_accounts = crate::interface::validation::extract_tail_accounts( - remaining_accounts, - params.compressed_accounts.len(), - )?; - - for (i, account_data) in params.compressed_accounts.iter().enumerate() { - let pda_account = &pda_accounts[i]; - - // Skip empty accounts or accounts not owned by this program - if crate::interface::validation::should_skip_compression(pda_account, program_id) { - continue; - } - - // Delegate to dispatch callback (macro-generated match) - dispatch_fn(pda_account, account_data, i, &mut compress_ctx)?; - } - - // If any account is not yet compressible, skip the entire batch. - // The proof covers all accounts so we cannot partially compress. - if compress_ctx.has_non_compressible { - return Ok(()); - } - - // CPI to Light System Program - if !compress_ctx.compressed_account_infos.is_empty() { - LightSystemProgramCpi::new_cpi(cpi_signer, params.proof) - .with_account_infos(&compress_ctx.compressed_account_infos) - .invoke(cpi_accounts.clone()) - .map_err(|e| { - solana_msg::msg!("compress: CPI failed: {:?}", e); - ProgramError::Custom(200) - })?; - - // Close the PDA accounts - for idx in compress_ctx.pda_indices_to_close { - let mut info = pda_accounts[idx].clone(); - crate::interface::close::close(&mut info, rent_sponsor).map_err(ProgramError::from)?; - } - } - - Ok(()) -} diff --git a/sdk-libs/sdk/src/interface/program/config/create.rs b/sdk-libs/sdk/src/interface/program/config/create.rs deleted file mode 100644 index e6f87d1128..0000000000 --- a/sdk-libs/sdk/src/interface/program/config/create.rs +++ /dev/null @@ -1,301 +0,0 @@ -//! Config initialization instructions. - -use light_account_checks::discriminator::{Discriminator, DISCRIMINATOR_LEN}; -use light_compressible::rent::RentConfig; -use solana_account_info::AccountInfo; -use solana_cpi::invoke_signed; -use solana_loader_v3_interface::state::UpgradeableLoaderState; -use solana_msg::msg; -use solana_pubkey::Pubkey; -use solana_system_interface::instruction as system_instruction; -use solana_sysvar::{rent::Rent, Sysvar}; - -use super::{state::LightConfig, validate_address_space_no_duplicates, COMPRESSIBLE_CONFIG_SEED}; -use crate::{error::LightSdkError, light_account_checks::checks::check_signer, AnchorSerialize}; - -const BPF_LOADER_UPGRADEABLE_ID: Pubkey = - Pubkey::from_str_const("BPFLoaderUpgradeab1e11111111111111111111111"); - -/// Creates a new compressible config PDA -/// -/// # Security - Solana Best Practice -/// This function follows the standard Solana pattern where only the program's -/// upgrade authority can create the initial config. This prevents unauthorized -/// parties from hijacking the config system. -/// -/// # Arguments -/// * `config_account` - The config PDA account to initialize -/// * `update_authority` - Authority that can update the config after creation -/// * `rent_sponsor` - Account that receives rent from compressed PDAs -/// * `compression_authority` - Authority that can compress/close PDAs -/// * `rent_config` - Rent function parameters -/// * `write_top_up` - Lamports to top up on each write -/// * `address_space` - Address space for compressed accounts (currently 1 address_tree allowed) -/// * `config_bump` - Config bump seed (must be 0 for now) -/// * `payer` - Account paying for the PDA creation -/// * `system_program` - System program -/// * `program_id` - The program that owns the config -/// -/// # Required Validation (must be done by caller) -/// The caller MUST validate that the signer is the program's upgrade authority -/// by checking against the program data account. This cannot be done in the SDK -/// due to dependency constraints. -/// -/// # Returns -/// * `Ok(())` if config was created successfully -/// * `Err(ProgramError)` if there was an error -#[allow(clippy::too_many_arguments)] -pub fn process_initialize_light_config<'info>( - config_account: &AccountInfo<'info>, - update_authority: &AccountInfo<'info>, - rent_sponsor: &Pubkey, - compression_authority: &Pubkey, - rent_config: RentConfig, - write_top_up: u32, - address_space: Vec, - config_bump: u8, - payer: &AccountInfo<'info>, - system_program: &AccountInfo<'info>, - program_id: &Pubkey, -) -> Result<(), crate::ProgramError> { - // CHECK: only 1 address_space - if config_bump != 0 { - msg!("Config bump must be 0 for now, found: {}", config_bump); - return Err(LightSdkError::ConstraintViolation.into()); - } - - // CHECK: not already initialized - if config_account.data_len() > 0 { - msg!("Config account already initialized"); - return Err(LightSdkError::ConstraintViolation.into()); - } - - // CHECK: only 1 address_space - if address_space.len() != 1 { - msg!( - "Address space must contain exactly 1 pubkey, found: {}", - address_space.len() - ); - return Err(LightSdkError::ConstraintViolation.into()); - } - - // CHECK: unique pubkeys in address_space - validate_address_space_no_duplicates(&address_space)?; - - // CHECK: signer - check_signer(update_authority).inspect_err(|_| { - msg!("Update authority must be signer for initial config creation"); - })?; - - // CHECK: pda derivation - let (derived_pda, bump) = LightConfig::derive_pda(program_id, config_bump); - if derived_pda != *config_account.key { - msg!("Invalid config PDA"); - return Err(LightSdkError::ConstraintViolation.into()); - } - - // Derive rent_sponsor_bump for storage - let (derived_rent_sponsor, rent_sponsor_bump) = - LightConfig::derive_rent_sponsor_pda(program_id); - if *rent_sponsor != derived_rent_sponsor { - msg!( - "rent_sponsor must be derived PDA: expected {:?}, got {:?}", - derived_rent_sponsor, - rent_sponsor - ); - return Err(LightSdkError::InvalidRentSponsor.into()); - } - - let rent = Rent::get().map_err(LightSdkError::from)?; - let account_size = LightConfig::size_for_address_space(address_space.len()); - let rent_lamports = rent.minimum_balance(account_size); - - // Use u16 to_le_bytes to match derive_pda (2 bytes instead of 1) - let config_bump_bytes = (config_bump as u16).to_le_bytes(); - let seeds = &[ - COMPRESSIBLE_CONFIG_SEED, - config_bump_bytes.as_ref(), - &[bump], - ]; - let create_account_ix = system_instruction::create_account( - payer.key, - config_account.key, - rent_lamports, - account_size as u64, - program_id, - ); - - invoke_signed( - &create_account_ix, - &[ - payer.clone(), - config_account.clone(), - system_program.clone(), - ], - &[seeds], - ) - .map_err(LightSdkError::from)?; - - let config = LightConfig { - version: 1, - write_top_up, - update_authority: *update_authority.key, - rent_sponsor: *rent_sponsor, - compression_authority: *compression_authority, - rent_config, - config_bump, - bump, - rent_sponsor_bump, - address_space, - }; - - let mut data = config_account - .try_borrow_mut_data() - .map_err(LightSdkError::from)?; - - // Write discriminator first (using trait constant) - data[..DISCRIMINATOR_LEN].copy_from_slice(&LightConfig::LIGHT_DISCRIMINATOR); - - // Serialize config data after discriminator - config - .serialize(&mut &mut data[DISCRIMINATOR_LEN..]) - .map_err(|_| LightSdkError::Borsh)?; - - Ok(()) -} - -/// Checks that the signer is the program's upgrade authority -/// -/// # Arguments -/// * `program_id` - The program to check -/// * `program_data_account` - The program's data account (ProgramData) -/// * `authority` - The authority to verify -/// -/// # Returns -/// * `Ok(())` if authority is valid -/// * `Err(LightSdkError)` if authority is invalid or verification fails -pub fn check_program_upgrade_authority( - program_id: &Pubkey, - program_data_account: &AccountInfo, - authority: &AccountInfo, -) -> Result<(), crate::ProgramError> { - // CHECK: program data PDA - let (expected_program_data, _) = - Pubkey::find_program_address(&[program_id.as_ref()], &BPF_LOADER_UPGRADEABLE_ID); - if program_data_account.key != &expected_program_data { - msg!("Invalid program data account"); - return Err(LightSdkError::ConstraintViolation.into()); - } - - let data = program_data_account.try_borrow_data()?; - let program_state: UpgradeableLoaderState = bincode::deserialize(&data).map_err(|_| { - msg!("Failed to deserialize program data account"); - LightSdkError::ConstraintViolation - })?; - - // Extract upgrade authority - let upgrade_authority = match program_state { - UpgradeableLoaderState::ProgramData { - slot: _, - upgrade_authority_address, - } => { - match upgrade_authority_address { - Some(auth) => { - // Check for invalid zero authority when authority exists - if auth == Pubkey::default() { - msg!("Invalid state: authority is zero pubkey"); - return Err(LightSdkError::ConstraintViolation.into()); - } - auth - } - None => { - msg!("Program has no upgrade authority"); - return Err(LightSdkError::ConstraintViolation.into()); - } - } - } - _ => { - msg!("Account is not ProgramData, found: {:?}", program_state); - return Err(LightSdkError::ConstraintViolation.into()); - } - }; - - // CHECK: upgrade authority is signer - check_signer(authority).inspect_err(|_| { - msg!("Authority must be signer"); - })?; - - // CHECK: upgrade authority is program's upgrade authority - if *authority.key != upgrade_authority { - msg!( - "Signer is not the program's upgrade authority. Signer: {:?}, Expected Authority: {:?}", - authority.key, - upgrade_authority - ); - return Err(LightSdkError::ConstraintViolation.into()); - } - - Ok(()) -} - -/// Creates a new compressible config PDA. -/// -/// # Arguments -/// * `config_account` - The config PDA account to initialize -/// * `update_authority` - Must be the program's upgrade authority -/// * `program_data_account` - The program's data account for validation -/// * `rent_sponsor` - Account that receives rent from compressed PDAs -/// * `compression_authority` - Authority that can compress/close PDAs -/// * `rent_config` - Rent function parameters -/// * `write_top_up` - Lamports to top up on each write -/// * `address_space` - Address spaces for compressed accounts (exactly 1 -/// allowed) -/// * `config_bump` - Config bump seed (must be 0 for now) -/// * `payer` - Account paying for the PDA creation -/// * `system_program` - System program -/// * `program_id` - The program that owns the config -/// -/// # Returns -/// * `Ok(())` if config was created successfully -/// * `Err(ProgramError)` if there was an error or authority validation fails -#[allow(clippy::too_many_arguments)] -pub fn process_initialize_light_config_checked<'info>( - config_account: &AccountInfo<'info>, - update_authority: &AccountInfo<'info>, - program_data_account: &AccountInfo<'info>, - rent_sponsor: &Pubkey, - compression_authority: &Pubkey, - rent_config: RentConfig, - write_top_up: u32, - address_space: Vec, - config_bump: u8, - payer: &AccountInfo<'info>, - system_program: &AccountInfo<'info>, - program_id: &Pubkey, -) -> Result<(), crate::ProgramError> { - msg!( - "create_compression_config_checked program_data_account: {:?}", - program_data_account.key - ); - msg!( - "create_compression_config_checked program_id: {:?}", - program_id - ); - // Verify the signer is the program's upgrade authority - check_program_upgrade_authority(program_id, program_data_account, update_authority)?; - - // Create the config with validated authority - process_initialize_light_config( - config_account, - update_authority, - rent_sponsor, - compression_authority, - rent_config, - write_top_up, - address_space, - config_bump, - payer, - system_program, - program_id, - ) -} diff --git a/sdk-libs/sdk/src/interface/program/config/mod.rs b/sdk-libs/sdk/src/interface/program/config/mod.rs deleted file mode 100644 index 30a7975f5c..0000000000 --- a/sdk-libs/sdk/src/interface/program/config/mod.rs +++ /dev/null @@ -1,62 +0,0 @@ -//! LightConfig management for compressible accounts. - -use std::collections::HashSet; - -use solana_msg::msg; -use solana_pubkey::Pubkey; - -use crate::error::LightSdkError; - -mod create; -mod state; -mod update; - -// --- Constants --- - -pub const COMPRESSIBLE_CONFIG_SEED: &[u8] = b"compressible_config"; -pub const MAX_ADDRESS_TREES_PER_SPACE: usize = 1; - -// Re-export from sdk-types -// --- Re-exports --- -pub use create::{ - check_program_upgrade_authority, process_initialize_light_config, - process_initialize_light_config_checked, -}; -// Re-export Discriminator trait so users can access LightConfig::LIGHT_DISCRIMINATOR -pub use light_account_checks::discriminator::Discriminator; -pub use light_sdk_types::constants::RENT_SPONSOR_SEED; -pub use state::LightConfig; -pub use update::process_update_light_config; - -// --- Shared validators (used by create and update) --- - -/// Validates that address_space contains no duplicate pubkeys -pub(super) fn validate_address_space_no_duplicates( - address_space: &[Pubkey], -) -> Result<(), LightSdkError> { - let mut seen = HashSet::new(); - for pubkey in address_space { - if !seen.insert(pubkey) { - msg!("Duplicate pubkey found in address_space: {}", pubkey); - return Err(LightSdkError::ConstraintViolation); - } - } - Ok(()) -} - -/// Validates that new_address_space only adds to existing address_space (no removals) -pub(super) fn validate_address_space_only_adds( - existing_address_space: &[Pubkey], - new_address_space: &[Pubkey], -) -> Result<(), LightSdkError> { - for existing_pubkey in existing_address_space { - if !new_address_space.contains(existing_pubkey) { - msg!( - "Cannot remove existing pubkey from address_space: {}", - existing_pubkey - ); - return Err(LightSdkError::ConstraintViolation); - } - } - Ok(()) -} diff --git a/sdk-libs/sdk/src/interface/program/config/state.rs b/sdk-libs/sdk/src/interface/program/config/state.rs deleted file mode 100644 index 8f1074892f..0000000000 --- a/sdk-libs/sdk/src/interface/program/config/state.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! LightConfig state struct and methods. - -use light_account_checks::{ - checks::check_discriminator, - discriminator::{Discriminator, DISCRIMINATOR_LEN}, -}; -use light_compressible::rent::RentConfig; -use solana_account_info::AccountInfo; -use solana_msg::msg; -use solana_pubkey::Pubkey; - -use super::{COMPRESSIBLE_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE, RENT_SPONSOR_SEED}; -use crate::{error::LightSdkError, AnchorDeserialize, AnchorSerialize}; - -/// Global configuration for compressible accounts -#[derive(Clone, AnchorDeserialize, AnchorSerialize, Debug)] -pub struct LightConfig { - /// Config version for future upgrades - pub version: u8, - /// Lamports to top up on each write (heuristic) - pub write_top_up: u32, - /// Authority that can update the config - pub update_authority: Pubkey, - /// Account that receives rent from compressed PDAs - pub rent_sponsor: Pubkey, - /// Authority that can compress/close PDAs (distinct from rent_sponsor) - pub compression_authority: Pubkey, - /// Rent function parameters for compressibility and distribution - pub rent_config: RentConfig, - /// Config bump seed (0) - pub config_bump: u8, - /// Config PDA bump seed - pub bump: u8, - /// Rent sponsor PDA bump seed - pub rent_sponsor_bump: u8, - /// Address space for compressed accounts (currently 1 address_tree allowed) - pub address_space: Vec, -} - -/// Implement the Light Discriminator trait for LightConfig -impl Discriminator for LightConfig { - const LIGHT_DISCRIMINATOR: [u8; 8] = *b"LightCfg"; - const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; -} - -impl LightConfig { - /// Total account size including discriminator - pub const LEN: usize = DISCRIMINATOR_LEN - + 1 - + 4 - + 32 - + 32 - + 32 - + core::mem::size_of::() - + 1 - + 1 - + 1 - + 4 - + (32 * MAX_ADDRESS_TREES_PER_SPACE); - - /// Calculate the exact size needed for a LightConfig with the given - /// number of address spaces (includes discriminator) - pub fn size_for_address_space(num_address_trees: usize) -> usize { - DISCRIMINATOR_LEN - + 1 - + 4 - + 32 - + 32 - + 32 - + core::mem::size_of::() - + 1 - + 1 - + 1 - + 4 - + (32 * num_address_trees) - } - - /// Derives the config PDA address with config bump - pub fn derive_pda(program_id: &Pubkey, config_bump: u8) -> (Pubkey, u8) { - // Convert u8 to u16 to match program-libs derivation (uses u16 with to_le_bytes) - let config_bump_u16 = config_bump as u16; - Pubkey::find_program_address( - &[COMPRESSIBLE_CONFIG_SEED, &config_bump_u16.to_le_bytes()], - program_id, - ) - } - - /// Derives the default config PDA address (config_bump = 0) - pub fn derive_default_pda(program_id: &Pubkey) -> (Pubkey, u8) { - Self::derive_pda(program_id, 0) - } - - /// Derives the rent sponsor PDA address for a program. - /// Seeds: ["rent_sponsor"] - pub fn derive_rent_sponsor_pda(program_id: &Pubkey) -> (Pubkey, u8) { - Pubkey::find_program_address(&[RENT_SPONSOR_SEED], program_id) - } - - /// Validates rent_sponsor matches config and returns stored bump for signing. - pub fn validate_rent_sponsor( - &self, - rent_sponsor: &AccountInfo, - ) -> Result { - if *rent_sponsor.key != self.rent_sponsor { - msg!( - "rent_sponsor mismatch: expected {:?}, got {:?}", - self.rent_sponsor, - rent_sponsor.key - ); - return Err(LightSdkError::InvalidRentSponsor.into()); - } - Ok(self.rent_sponsor_bump) - } - - /// Checks the config account - pub fn validate(&self) -> Result<(), crate::ProgramError> { - if self.version != 1 { - msg!( - "LightConfig validation failed: Unsupported config version: {}", - self.version - ); - return Err(LightSdkError::ConstraintViolation.into()); - } - if self.address_space.len() != 1 { - msg!( - "LightConfig validation failed: Address space must contain exactly 1 pubkey, found: {}", - self.address_space.len() - ); - return Err(LightSdkError::ConstraintViolation.into()); - } - // For now, only allow config_bump = 0 to keep it simple - if self.config_bump != 0 { - msg!( - "LightConfig validation failed: Config bump must be 0 for now, found: {}", - self.config_bump - ); - return Err(LightSdkError::ConstraintViolation.into()); - } - Ok(()) - } - - /// Loads and validates config from account, checking owner, discriminator, and PDA derivation - #[inline(never)] - pub fn load_checked( - account: &AccountInfo, - program_id: &Pubkey, - ) -> Result { - // CHECK: Owner - if account.owner != program_id { - msg!( - "LightConfig::load_checked failed: Config account owner mismatch. Expected: {:?}. Found: {:?}.", - program_id, - account.owner - ); - return Err(LightSdkError::ConstraintViolation.into()); - } - - let data = account.try_borrow_data()?; - - // CHECK: Discriminator using light-account-checks - check_discriminator::(&data).map_err(|e| { - msg!("LightConfig::load_checked failed: {:?}", e); - LightSdkError::ConstraintViolation - })?; - - // Deserialize from offset after discriminator - let config = Self::try_from_slice(&data[DISCRIMINATOR_LEN..]).map_err(|err| { - msg!( - "LightConfig::load_checked failed: Failed to deserialize config data: {:?}", - err - ); - LightSdkError::Borsh - })?; - config.validate()?; - - // CHECK: PDA derivation - let (expected_pda, _) = Self::derive_pda(program_id, config.config_bump); - if expected_pda != *account.key { - msg!( - "LightConfig::load_checked failed: Config account key mismatch. Expected PDA: {:?}. Found: {:?}.", - expected_pda, - account.key - ); - return Err(LightSdkError::ConstraintViolation.into()); - } - - Ok(config) - } -} diff --git a/sdk-libs/sdk/src/interface/program/config/update.rs b/sdk-libs/sdk/src/interface/program/config/update.rs deleted file mode 100644 index 9d70336f8e..0000000000 --- a/sdk-libs/sdk/src/interface/program/config/update.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Config update instruction. - -use light_account_checks::discriminator::DISCRIMINATOR_LEN; -use light_compressible::rent::RentConfig; -use solana_account_info::AccountInfo; -use solana_msg::msg; -use solana_pubkey::Pubkey; - -use super::{ - state::LightConfig, validate_address_space_no_duplicates, validate_address_space_only_adds, - MAX_ADDRESS_TREES_PER_SPACE, -}; -use crate::{error::LightSdkError, light_account_checks::checks::check_signer, AnchorSerialize}; - -/// Updates an existing compressible config -/// -/// # Arguments -/// * `config_account` - The config PDA account to update -/// * `authority` - Current update authority (must match config) -/// * `new_update_authority` - Optional new update authority -/// * `new_rent_sponsor` - Optional new rent recipient -/// * `new_compression_authority` - Optional new compression authority -/// * `new_rent_config` - Optional new rent function parameters -/// * `new_write_top_up` - Optional new write top-up amount -/// * `new_address_space` - Optional new address space (currently 1 address_tree allowed) -/// * `owner_program_id` - The program that owns the config -/// -/// # Returns -/// * `Ok(())` if config was updated successfully -/// * `Err(ProgramError)` if there was an error -#[allow(clippy::too_many_arguments)] -pub fn process_update_light_config<'info>( - config_account: &AccountInfo<'info>, - authority: &AccountInfo<'info>, - new_update_authority: Option<&Pubkey>, - new_rent_sponsor: Option<&Pubkey>, - new_compression_authority: Option<&Pubkey>, - new_rent_config: Option, - new_write_top_up: Option, - new_address_space: Option>, - owner_program_id: &Pubkey, -) -> Result<(), crate::ProgramError> { - // CHECK: PDA derivation - let mut config = LightConfig::load_checked(config_account, owner_program_id)?; - - // CHECK: signer - check_signer(authority).inspect_err(|_| { - msg!("Update authority must be signer"); - })?; - // CHECK: authority - if *authority.key != config.update_authority { - msg!("Invalid update authority"); - return Err(LightSdkError::ConstraintViolation.into()); - } - - if let Some(new_authority) = new_update_authority { - config.update_authority = *new_authority; - } - if let Some(new_recipient) = new_rent_sponsor { - config.rent_sponsor = *new_recipient; - } - if let Some(new_auth) = new_compression_authority { - config.compression_authority = *new_auth; - } - if let Some(new_rcfg) = new_rent_config { - config.rent_config = new_rcfg; - } - if let Some(new_top_up) = new_write_top_up { - config.write_top_up = new_top_up; - } - if let Some(new_address_space) = new_address_space { - // CHECK: address space length - if new_address_space.len() != MAX_ADDRESS_TREES_PER_SPACE { - msg!( - "New address space must contain exactly 1 pubkey, found: {}", - new_address_space.len() - ); - return Err(LightSdkError::ConstraintViolation.into()); - } - - validate_address_space_no_duplicates(&new_address_space)?; - - validate_address_space_only_adds(&config.address_space, &new_address_space)?; - - config.address_space = new_address_space; - } - - let mut data = config_account.try_borrow_mut_data().map_err(|e| { - msg!("Failed to borrow mut data for config_account: {:?}", e); - LightSdkError::from(e) - })?; - // Serialize after discriminator (discriminator is preserved from init) - config - .serialize(&mut &mut data[DISCRIMINATOR_LEN..]) - .map_err(|e| { - msg!("Failed to serialize updated config: {:?}", e); - LightSdkError::Borsh - })?; - - Ok(()) -} diff --git a/sdk-libs/sdk/src/interface/program/decompression/create_token_account.rs b/sdk-libs/sdk/src/interface/program/decompression/create_token_account.rs deleted file mode 100644 index ef03341a25..0000000000 --- a/sdk-libs/sdk/src/interface/program/decompression/create_token_account.rs +++ /dev/null @@ -1,160 +0,0 @@ -//! ATA and token account creation helpers for decompression. - -use light_token_interface::instructions::{ - create_token_account::CreateTokenAccountInstructionData, - extensions::{CompressToPubkey, CompressibleExtensionInstructionData}, -}; -use solana_instruction::{AccountMeta, Instruction}; -use solana_program_error::ProgramError; -use solana_pubkey::Pubkey; - -use crate::AnchorSerialize; - -/// Build a CreateAssociatedTokenAccountIdempotent instruction for ATA decompression. -/// -/// Creates a compressible ATA with compression_only mode (required for ATA decompression). -/// -/// # Account order (per on-chain handler): -/// 0. owner (non-mut, non-signer) - The wallet owner -/// 1. mint (non-mut, non-signer) - The token mint -/// 2. fee_payer (signer, writable) - Pays for account creation -/// 3. associated_token_account (writable, NOT signer) - The ATA to create -/// 4. system_program (readonly) - System program -/// 5. compressible_config (readonly) - Compressible config PDA -/// 6. rent_payer (writable) - Rent sponsor account -/// -/// # Arguments -/// * `wallet_owner` - The wallet owner (ATA derivation seed) -/// * `mint` - The token mint -/// * `fee_payer` - Pays for account creation -/// * `ata` - The ATA pubkey (derived from wallet_owner, program_id, mint) -/// * `bump` - The ATA derivation bump -/// * `compressible_config` - Compressible config PDA -/// * `rent_sponsor` - Rent sponsor account -/// * `write_top_up` - Lamports per write for top-up -#[allow(clippy::too_many_arguments)] -pub fn build_create_ata_instruction( - wallet_owner: &Pubkey, - mint: &Pubkey, - fee_payer: &Pubkey, - ata: &Pubkey, - bump: u8, - compressible_config: &Pubkey, - rent_sponsor: &Pubkey, - write_top_up: u32, -) -> Result { - use light_token_interface::instructions::{ - create_associated_token_account::CreateAssociatedTokenAccountInstructionData, - extensions::CompressibleExtensionInstructionData, - }; - - let instruction_data = CreateAssociatedTokenAccountInstructionData { - bump, - compressible_config: Some(CompressibleExtensionInstructionData { - token_account_version: 3, // ShaFlat version (required) - rent_payment: 16, // 24h, TODO: make configurable - compression_only: 1, // Required for ATA - write_top_up, - compress_to_account_pubkey: None, // Required to be None for ATA - }), - }; - - let mut data = Vec::new(); - data.push(102u8); // CreateAssociatedTokenAccountIdempotent discriminator - instruction_data - .serialize(&mut data) - .map_err(|e| ProgramError::BorshIoError(e.to_string()))?; - - let accounts = vec![ - AccountMeta::new_readonly(*wallet_owner, false), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new(*fee_payer, true), - AccountMeta::new(*ata, false), // NOT a signer - ATA is derived - AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(*compressible_config, false), - AccountMeta::new(*rent_sponsor, false), - ]; - - Ok(Instruction { - program_id: light_token_interface::LIGHT_TOKEN_PROGRAM_ID.into(), - accounts, - data, - }) -} - -/// Build a CreateTokenAccount instruction for decompression. -/// -/// Creates a compressible token account with ShaFlat version (required by light token program). -/// -/// # Account order: -/// 0. token_account (signer, writable) - The token account PDA to create -/// 1. mint (readonly) - The token mint -/// 2. fee_payer (signer, writable) - Pays for account creation -/// 3. compressible_config (readonly) - Compressible config PDA -/// 4. system_program (readonly) - System program -/// 5. rent_sponsor (writable) - Rent sponsor account -/// -/// # Arguments -/// * `signer_seeds` - Seeds including bump for the token account PDA -/// * `program_id` - Program ID that owns the token account PDA -#[allow(clippy::too_many_arguments)] -pub fn build_create_token_account_instruction( - token_account: &Pubkey, - mint: &Pubkey, - owner: &Pubkey, - fee_payer: &Pubkey, - compressible_config: &Pubkey, - rent_sponsor: &Pubkey, - write_top_up: u32, - signer_seeds: &[&[u8]], - program_id: &Pubkey, -) -> Result { - // Build CompressToPubkey from signer_seeds (last seed is bump) - let bump = signer_seeds - .last() - .and_then(|s| s.first().copied()) - .ok_or(ProgramError::InvalidSeeds)?; - let seeds_without_bump: Vec> = signer_seeds - .iter() - .take(signer_seeds.len().saturating_sub(1)) - .map(|s| s.to_vec()) - .collect(); - - let compress_to_account_pubkey = CompressToPubkey { - bump, - program_id: program_id.to_bytes(), - seeds: seeds_without_bump, - }; - - let instruction_data = CreateTokenAccountInstructionData { - owner: light_compressed_account::Pubkey::from(owner.to_bytes()), - compressible_config: Some(CompressibleExtensionInstructionData { - token_account_version: 3, // ShaFlat version (required) - rent_payment: 16, // 24h, TODO: make configurable - compression_only: 0, // Regular tokens can be transferred, not compression-only - write_top_up, - compress_to_account_pubkey: Some(compress_to_account_pubkey), - }), - }; - - let mut data = Vec::new(); - data.push(18u8); // InitializeAccount3 opcode - instruction_data - .serialize(&mut data) - .map_err(|e| ProgramError::BorshIoError(e.to_string()))?; - - let accounts = vec![ - AccountMeta::new(*token_account, true), - AccountMeta::new_readonly(*mint, false), - AccountMeta::new(*fee_payer, true), - AccountMeta::new_readonly(*compressible_config, false), - AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new(*rent_sponsor, false), - ]; - - Ok(Instruction { - program_id: light_token_interface::LIGHT_TOKEN_PROGRAM_ID.into(), - accounts, - data, - }) -} diff --git a/sdk-libs/sdk/src/interface/program/decompression/processor.rs b/sdk-libs/sdk/src/interface/program/decompression/processor.rs deleted file mode 100644 index c59c17ed9c..0000000000 --- a/sdk-libs/sdk/src/interface/program/decompression/processor.rs +++ /dev/null @@ -1,407 +0,0 @@ -//! SDK generic decompression functions. -//! -//! These functions are generic over account types and can be reused by the macro. -//! The decompress flow creates PDAs from compressed state (needs validity proof, packed data, seeds). - -use anchor_lang::{ - prelude::*, - solana_program::{clock::Clock, program::invoke_signed, rent::Rent, sysvar::Sysvar}, -}; -use light_compressed_account::instruction_data::{ - cpi_context::CompressedCpiContext, with_account_info::CompressedAccountInfo, -}; -#[cfg(feature = "cpi-context")] -use light_sdk_types::cpi_context_write::CpiContextWriteAccounts; -use light_sdk_types::{ - cpi_accounts::CpiAccountsConfig, instruction::PackedStateTreeInfo, CpiSigner, - ACCOUNT_COMPRESSION_AUTHORITY_PDA, ACCOUNT_COMPRESSION_PROGRAM_ID, LIGHT_SYSTEM_PROGRAM_ID, - REGISTERED_PROGRAM_PDA, -}; -use light_token_interface::{ - instructions::{ - extensions::ExtensionInstructionData, - transfer2::{ - CompressedTokenInstructionDataTransfer2, Compression, MultiInputTokenDataWithContext, - }, - }, - CPI_AUTHORITY, LIGHT_TOKEN_PROGRAM_ID, TRANSFER2, -}; -use solana_instruction::Instruction; -use solana_program_error::ProgramError; - -use crate::{ - cpi::{v2::CpiAccounts, InvokeLightSystemProgram}, - instruction::ValidityProof, - interface::{compression_info::CompressedAccountData, LightConfig}, -}; - -// ============================================================================ -// DecompressVariant Trait (implemented by program's PackedProgramAccountVariant) -// ============================================================================ - -/// Trait for packed program account variants that support decompression. -/// -/// This trait is implemented by the program's `PackedProgramAccountVariant` enum -/// to handle type-specific dispatch during decompression. -/// -/// MACRO-GENERATED: The implementation contains a match statement routing each -/// enum variant to the appropriate `prepare_account_for_decompression` call. -pub trait DecompressVariant<'info>: AnchorSerialize + AnchorDeserialize + Clone { - /// Decompress this variant into a PDA account. - /// - /// The implementation should match on the enum variant and call - /// `prepare_account_for_decompression::(packed, pda_account, ctx)`. - fn decompress( - &self, - meta: &PackedStateTreeInfo, - pda_account: &AccountInfo<'info>, - ctx: &mut DecompressCtx<'_, 'info>, - ) -> std::result::Result<(), ProgramError>; -} - -// ============================================================================ -// Parameters and Context -// ============================================================================ - -/// Parameters for decompress_idempotent instruction. -/// Generic over the variant type - each program defines its own `PackedProgramAccountVariant`. -/// -/// Field order matches `LoadAccountsData` from light-client for compatibility. -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct DecompressIdempotentParams -where - V: AnchorSerialize + AnchorDeserialize + Clone, -{ - /// Offset into remaining_accounts where Light system accounts begin - pub system_accounts_offset: u8, - /// All account variants less than offset are pda acccounts. - /// 255 if no token accounts - pub token_accounts_offset: u8, - /// Packed index of the output queue in remaining_accounts. - pub output_queue_index: u8, - /// Validity proof for compressed account verification - pub proof: ValidityProof, - /// Accounts to decompress - wrapped in CompressedAccountData for metadata - pub accounts: Vec>, -} - -/// Context struct holding all data needed for decompression. -/// Contains internal vec for collecting CompressedAccountInfo results. -pub struct DecompressCtx<'a, 'info> { - pub program_id: &'a Pubkey, - pub cpi_accounts: &'a CpiAccounts<'a, 'info>, - pub remaining_accounts: &'a [AccountInfo<'info>], - pub rent_sponsor: &'a AccountInfo<'info>, - /// Rent sponsor PDA bump for signing - pub rent_sponsor_bump: u8, - pub light_config: &'a LightConfig, - /// Token (ctoken) rent sponsor for creating token accounts - pub ctoken_rent_sponsor: &'a AccountInfo<'info>, - /// Token (ctoken) compressible config for creating token accounts - pub ctoken_compressible_config: &'a AccountInfo<'info>, - pub rent: &'a Rent, - pub current_slot: u64, - /// Packed index of the output queue in remaining_accounts. - pub output_queue_index: u8, - /// Internal vec - dispatch functions push results here - pub compressed_account_infos: Vec, - pub in_token_data: Vec, - pub in_tlv: Option>>, - pub token_seeds: Vec>, -} - -// ============================================================================ -// Processor Function -// ============================================================================ - -/// Remaining accounts layout: -/// [0]: fee_payer (Signer, mut) -/// [1]: config (LightConfig PDA) -/// [2]: rent_sponsor (mut) -/// [system_accounts_offset..]: Light system accounts for CPI -/// [remaining_accounts.len() - num_pda_accounts..]: PDA accounts to decompress -/// -/// Runtime processor - handles all the plumbing, dispatches via DecompressVariant trait. -/// -/// **Takes raw instruction data** and deserializes internally - minimizes macro code. -/// **Uses only remaining_accounts** - no Context struct needed. -/// **Generic over V** - the program's `PackedProgramAccountVariant` enum. -pub fn process_decompress_pda_accounts_idempotent<'info, V>( - remaining_accounts: &[AccountInfo<'info>], - instruction_data: &[u8], - cpi_signer: CpiSigner, - program_id: &Pubkey, -) -> std::result::Result<(), ProgramError> -where - V: DecompressVariant<'info>, -{ - // Deserialize params internally - let params = DecompressIdempotentParams::::try_from_slice(instruction_data) - .map_err(|_| ProgramError::InvalidInstructionData)?; - - // Extract and validate accounts using shared validation - let validated_ctx = - crate::interface::validation::validate_decompress_accounts(remaining_accounts, program_id)?; - let fee_payer = &validated_ctx.fee_payer; - let rent_sponsor = &validated_ctx.rent_sponsor; - let rent_sponsor_bump = validated_ctx.rent_sponsor_bump; - let light_config = validated_ctx.light_config; - - let rent = Rent::get()?; - let current_slot = Clock::get()?.slot; - - let system_accounts_offset_usize = params.system_accounts_offset as usize; - if system_accounts_offset_usize > remaining_accounts.len() { - return Err(ProgramError::InvalidInstructionData); - } - let (pda_accounts, token_accounts) = params - .accounts - .split_at_checked(params.token_accounts_offset as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - - // PDA and token account infos are at the tail of remaining_accounts. - let num_hot_accounts = params.accounts.len(); - let hot_accounts_start = remaining_accounts - .len() - .checked_sub(num_hot_accounts) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let hot_account_infos = &remaining_accounts[hot_accounts_start..]; - let (pda_account_infos, token_account_infos) = hot_account_infos - .split_at_checked(params.token_accounts_offset as usize) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - - let has_pda_accounts = !pda_accounts.is_empty(); - let has_token_accounts = !token_accounts.is_empty(); - let cpi_context = has_pda_accounts && has_token_accounts; - let config = CpiAccountsConfig { - sol_compression_recipient: false, - sol_pool_pda: false, - cpi_context, - cpi_signer, - }; - let cpi_accounts = CpiAccounts::new_with_config( - fee_payer, - &remaining_accounts[system_accounts_offset_usize..], - config, - ); - - // Token (ctoken) accounts layout in remaining_accounts: - // [0]fee_payer, [1]pda_config, [2]pda_rent_sponsor, [3]ctoken_rent_sponsor, - // [4]light_token_program, [5]cpi_authority, [6]ctoken_compressible_config - let ctoken_rent_sponsor = remaining_accounts - .get(3) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let ctoken_compressible_config = remaining_accounts - .get(6) - .ok_or(ProgramError::NotEnoughAccountKeys)?; - - // Build context struct with all needed data (includes internal vec) - let mut decompress_ctx = DecompressCtx { - program_id, - cpi_accounts: &cpi_accounts, - remaining_accounts, - rent_sponsor, - rent_sponsor_bump, - light_config: &light_config, - ctoken_rent_sponsor, - ctoken_compressible_config, - rent: &rent, - current_slot, - output_queue_index: params.output_queue_index, - compressed_account_infos: Vec::new(), - in_token_data: Vec::new(), - in_tlv: None, - token_seeds: Vec::new(), - }; - - // Process each account using trait dispatch on inner variant - for (pda_account, pda_account_info) in pda_accounts.iter().zip(pda_account_infos) { - pda_account.data.decompress( - &pda_account.tree_info, - pda_account_info, - &mut decompress_ctx, - )?; - } - // Process token accounts - for (token_account, token_account_info) in token_accounts.iter().zip(token_account_infos) { - token_account.data.decompress( - &token_account.tree_info, - token_account_info, - &mut decompress_ctx, - )?; - } - - if has_pda_accounts { - // CPI to Light System Program with proof - #[cfg(feature = "cpi-context")] - let pda_only = !cpi_context; - #[cfg(not(feature = "cpi-context"))] - let pda_only = true; - - if pda_only { - // Manual construction to avoid extra allocations - let instruction_data = light_compressed_account::instruction_data::with_account_info::InstructionDataInvokeCpiWithAccountInfo { - mode: 1, - bump: cpi_signer.bump, - invoking_program_id: cpi_signer.program_id.into(), - compress_or_decompress_lamports: 0, - is_compress: false, - with_cpi_context: false, - with_transaction_hash: false, - cpi_context: CompressedCpiContext::default(), - proof: params.proof.0, - new_address_params: Vec::new(), - account_infos: decompress_ctx.compressed_account_infos, - read_only_addresses: Vec::new(), - read_only_accounts: Vec::new(), - }; - instruction_data.invoke(cpi_accounts.clone())?; - } else { - #[cfg(feature = "cpi-context")] - { - // PDAs + tokens - write to CPI context first, tokens will execute - let authority = cpi_accounts - .authority() - .map_err(|_| ProgramError::MissingRequiredSignature)?; - let cpi_context_account = cpi_accounts - .cpi_context() - .map_err(|_| ProgramError::MissingRequiredSignature)?; - let system_cpi_accounts = CpiContextWriteAccounts { - fee_payer, - authority, - cpi_context: cpi_context_account, - cpi_signer, - }; - - // Manual construction to avoid extra allocations - let instruction_data = light_compressed_account::instruction_data::with_account_info::InstructionDataInvokeCpiWithAccountInfo { - mode: 1, - bump: cpi_signer.bump, - invoking_program_id: cpi_signer.program_id.into(), - compress_or_decompress_lamports: 0, - is_compress: false, - with_cpi_context: true, - with_transaction_hash: false, - cpi_context: CompressedCpiContext::first(), - proof: None, - new_address_params: Vec::new(), - account_infos: decompress_ctx.compressed_account_infos, - read_only_addresses: Vec::new(), - read_only_accounts: Vec::new(), - }; - instruction_data.invoke_write_to_cpi_context_first(system_cpi_accounts)?; - } - #[cfg(not(feature = "cpi-context"))] - { - return Err(ProgramError::InvalidInstructionData); - } - } - } - - if has_token_accounts { - let mut compressions = Vec::new(); - // Assumes is compressed to pubkey. - decompress_ctx - .in_token_data - .iter() - .for_each(|a| compressions.push(Compression::decompress(a.amount, a.mint, a.owner))); - let mut cpi = CompressedTokenInstructionDataTransfer2 { - with_transaction_hash: false, - in_token_data: decompress_ctx.in_token_data.clone(), - in_tlv: decompress_ctx.in_tlv.clone(), - with_lamports_change_account_merkle_tree_index: false, - lamports_change_account_merkle_tree_index: 0, - lamports_change_account_owner_index: 0, - output_queue: 0, - max_top_up: 0, - cpi_context: None, - compressions: Some(compressions), - proof: params.proof.0, - out_token_data: Vec::new(), - in_lamports: None, - out_lamports: None, - out_tlv: None, - }; - if has_pda_accounts { - cpi.cpi_context = Some( - light_token_interface::instructions::transfer2::CompressedCpiContext { - set_context: false, - first_set_context: false, - }, - ) - } - - // Build Transfer2 account_metas in the order the handler expects: - // [0] light_system_program (readonly) - // [1] fee_payer (signer, writable) - // [2] cpi_authority_pda (readonly) - // [3] registered_program_pda (readonly) - // [4] account_compression_authority (readonly) - // [5] account_compression_program (readonly) - // [6] system_program (readonly) - // [7] cpi_context (optional, writable) - // [N+] packed_accounts - let mut account_metas = vec![ - AccountMeta::new_readonly(Pubkey::new_from_array(LIGHT_SYSTEM_PROGRAM_ID), false), - AccountMeta::new(*fee_payer.key, true), - AccountMeta::new_readonly(Pubkey::new_from_array(CPI_AUTHORITY), false), - AccountMeta::new_readonly(Pubkey::new_from_array(REGISTERED_PROGRAM_PDA), false), - AccountMeta::new_readonly( - Pubkey::new_from_array(ACCOUNT_COMPRESSION_AUTHORITY_PDA), - false, - ), - AccountMeta::new_readonly( - Pubkey::new_from_array(ACCOUNT_COMPRESSION_PROGRAM_ID), - false, - ), - AccountMeta::new_readonly(Pubkey::default(), false), - ]; - if cpi_context { - let cpi_ctx = cpi_accounts - .cpi_context() - .map_err(|_| ProgramError::NotEnoughAccountKeys)?; - account_metas.push(AccountMeta::new(*cpi_ctx.key, false)); - } - let transfer2_packed_start = account_metas.len(); - let packed_accounts_offset = - system_accounts_offset_usize + cpi_accounts.system_accounts_end_offset(); - for account in &remaining_accounts[packed_accounts_offset..] { - account_metas.push(AccountMeta { - pubkey: *account.key, - is_signer: account.is_signer, - is_writable: account.is_writable, - }); - } - cpi.in_token_data.iter().for_each(|data| { - account_metas[data.owner as usize + transfer2_packed_start].is_signer = true; - }); - let mut instruction_data = vec![TRANSFER2]; - cpi.serialize(&mut instruction_data).unwrap(); - let instruction = Instruction { - program_id: LIGHT_TOKEN_PROGRAM_ID.into(), - accounts: account_metas, - data: instruction_data, - }; - // For ATAs, no PDA signing is needed (wallet owner signed at transaction level). - // For regular token accounts, use invoke_signed with PDA seeds. - if decompress_ctx.token_seeds.is_empty() { - // All tokens are ATAs - use regular invoke (no PDA signing needed) - anchor_lang::solana_program::program::invoke(&instruction, remaining_accounts)?; - } else { - // At least one regular token account - use invoke_signed with PDA seeds - let signer_seed_refs: Vec<&[u8]> = decompress_ctx - .token_seeds - .iter() - .map(|s| s.as_slice()) - .collect(); - - invoke_signed( - &instruction, - remaining_accounts, - &[signer_seed_refs.as_slice()], - )?; - } - } - - Ok(()) -} diff --git a/sdk-libs/sdk/src/interface/program/validation.rs b/sdk-libs/sdk/src/interface/program/validation.rs deleted file mode 100644 index 9efe406514..0000000000 --- a/sdk-libs/sdk/src/interface/program/validation.rs +++ /dev/null @@ -1,182 +0,0 @@ -//! Shared validation utilities for compress/decompress operations. - -use solana_account_info::AccountInfo; -use solana_program_error::ProgramError; -use solana_pubkey::Pubkey; - -use crate::{ - error::LightSdkError, - interface::LightConfig, - light_account_checks::{account_iterator::AccountIterator, checks::check_data_is_zeroed}, -}; - -/// Validated PDA context after account extraction and config validation. -pub struct ValidatedPdaContext<'info> { - pub fee_payer: AccountInfo<'info>, - pub light_config: LightConfig, - pub rent_sponsor: AccountInfo<'info>, - pub rent_sponsor_bump: u8, - /// Only present when EXTRACT_COMPRESSION_AUTHORITY=true - pub compression_authority: Option>, -} - -/// Extract and validate accounts for compress operations (4 accounts including compression_authority). -/// -/// # Account layout: -/// - `0` - fee_payer (Signer, mut) -/// - `1` - config (LightConfig PDA) -/// - `2` - rent_sponsor (mut) -/// - `3` - compression_authority (TODO: Signer when client-side code is updated) -pub fn validate_compress_accounts<'info>( - remaining_accounts: &[AccountInfo<'info>], - program_id: &Pubkey, -) -> Result, ProgramError> { - validate_pda_common_accounts_inner::(remaining_accounts, program_id) -} - -/// Extract and validate accounts for decompress operations (3 accounts, no compression_authority). -/// -/// # Account layout: -/// - `0` - fee_payer (Signer, mut) -/// - `1` - config (LightConfig PDA) -/// - `2` - rent_sponsor (mut) -pub fn validate_decompress_accounts<'info>( - remaining_accounts: &[AccountInfo<'info>], - program_id: &Pubkey, -) -> Result, ProgramError> { - validate_pda_common_accounts_inner::(remaining_accounts, program_id) -} - -/// Internal function with const generic for optional compression_authority extraction. -/// -/// # Security checks: -/// - fee_payer is signer and mutable -/// - config exists and is not mutable -/// - rent_sponsor is mutable -/// - compression_authority is extracted (if EXTRACT_COMPRESSION_AUTHORITY=true) -/// - LightConfig ownership matches program_id -/// - LightConfig PDA derivation is correct -/// - rent_sponsor matches config.rent_sponsor -/// - TODO: compression_authority matches config.compression_authority (when enabled) -fn validate_pda_common_accounts_inner<'info, const EXTRACT_COMPRESSION_AUTHORITY: bool>( - remaining_accounts: &[AccountInfo<'info>], - program_id: &Pubkey, -) -> Result, ProgramError> { - let mut account_iter = AccountIterator::new(remaining_accounts); - - let fee_payer = account_iter - .next_signer_mut("fee_payer") - .map_err(ProgramError::from)?; - let config = account_iter - .next_non_mut("config") - .map_err(ProgramError::from)?; - let rent_sponsor = account_iter - .next_mut("rent_sponsor") - .map_err(ProgramError::from)?; - - let compression_authority = if EXTRACT_COMPRESSION_AUTHORITY { - // TODO: make compression_authority a signer when client-side code is updated - Some( - account_iter - .next_account("compression_authority") - .map_err(ProgramError::from)? - .clone(), - ) - } else { - None - }; - - let light_config = LightConfig::load_checked(config, program_id) - .map_err(|_| ProgramError::InvalidAccountData)?; - - let rent_sponsor_bump = light_config - .validate_rent_sponsor(rent_sponsor) - .map_err(|_| LightSdkError::InvalidRentSponsor)?; - - // TODO: validate compression_authority matches config when client-side code is updated - // if EXTRACT_COMPRESSION_AUTHORITY { - // if let Some(ref auth) = compression_authority { - // if *auth.key != light_config.compression_authority { - // solana_msg::msg!( - // "compression_authority mismatch: expected {:?}, got {:?}", - // light_config.compression_authority, - // auth.key - // ); - // return Err(LightSdkError::ConstraintViolation.into()); - // } - // } - // } - - Ok(ValidatedPdaContext { - fee_payer: fee_payer.clone(), - light_config, - rent_sponsor: rent_sponsor.clone(), - rent_sponsor_bump, - compression_authority, - }) -} - -/// Validate and split remaining_accounts at system_accounts_offset. -/// -/// Returns (accounts_before_offset, accounts_from_offset). -pub fn split_at_system_accounts_offset<'a, 'info>( - remaining_accounts: &'a [AccountInfo<'info>], - system_accounts_offset: u8, -) -> Result<(&'a [AccountInfo<'info>], &'a [AccountInfo<'info>]), ProgramError> { - let offset = system_accounts_offset as usize; - remaining_accounts.split_at_checked(offset).ok_or_else(|| { - solana_msg::msg!( - "system_accounts_offset {} > len {}", - offset, - remaining_accounts.len() - ); - ProgramError::InvalidInstructionData - }) -} - -/// Extract PDA accounts from the tail of remaining_accounts. -pub fn extract_tail_accounts<'a, 'info>( - remaining_accounts: &'a [AccountInfo<'info>], - num_pda_accounts: usize, -) -> Result<&'a [AccountInfo<'info>], ProgramError> { - let start = remaining_accounts - .len() - .checked_sub(num_pda_accounts) - .ok_or_else(|| { - solana_msg::msg!( - "num_pda_accounts {} > len {}", - num_pda_accounts, - remaining_accounts.len() - ); - ProgramError::NotEnoughAccountKeys - })?; - Ok(&remaining_accounts[start..]) -} - -/// Check if PDA account is already initialized (has non-zero discriminator). -/// -/// Returns: -/// - `Ok(true)` if account has data and non-zero discriminator (initialized) -/// - `Ok(false)` if account is empty or has zeroed discriminator (not initialized) -pub fn is_pda_initialized(account: &AccountInfo) -> Result { - use crate::light_account_checks::discriminator::DISCRIMINATOR_LEN; - - if account.data_is_empty() { - return Ok(false); - } - let data = account.try_borrow_data()?; - if data.len() < DISCRIMINATOR_LEN { - return Ok(false); - } - // If discriminator is NOT zeroed, account is initialized - Ok(check_data_is_zeroed::(&data).is_err()) -} - -/// Check if account should be skipped during compression. -/// -/// Returns true if: -/// - Account has no data (empty) -/// - Account is not owned by the expected program -pub fn should_skip_compression(account: &AccountInfo, expected_owner: &Pubkey) -> bool { - account.data_is_empty() || account.owner != expected_owner -} diff --git a/sdk-libs/sdk/src/interface/program/variant.rs b/sdk-libs/sdk/src/interface/program/variant.rs deleted file mode 100644 index c199e24cb6..0000000000 --- a/sdk-libs/sdk/src/interface/program/variant.rs +++ /dev/null @@ -1,187 +0,0 @@ -//! Traits for decompression variant construction and manual Light Protocol implementation. -//! -//! This module contains traits for typed compressed account handling: -//! - Base traits (`IntoVariant`) - always available -//! - Variant traits (`LightAccountVariantTrait`, `PackedLightAccountVariantTrait`) - anchor-gated -//! - Token seed traits (`UnpackedTokenSeeds`, `PackedTokenSeeds`) - anchor-gated - -// --- Base traits (always available) --- - -#[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; -} - -// --- Anchor-gated variant traits --- - -#[cfg(feature = "anchor")] -mod anchor_traits { - use anchor_lang::prelude::*; - use light_sdk_types::instruction::PackedStateTreeInfo; - use light_token_interface::instructions::{ - extensions::ExtensionInstructionData, transfer2::MultiInputTokenDataWithContext, - }; - use solana_program_error::ProgramError; - - use super::super::super::account::light_account::AccountType; - - /// Trait for unpacked compressed account variants with seeds. - /// - /// Implementations are generated by the `#[light_program]` macro for each - /// account type marked with `#[light_account(init)]`. - /// - /// # Type Parameters - /// * `SEED_COUNT` - Number of seeds including bump for CPI signing - /// * `Seeds` - The seeds struct type (e.g., `UserRecordSeeds`) - /// * `Data` - The account data type (e.g., `UserRecord`) - /// * `Packed` - The packed variant type for serialization - pub trait LightAccountVariantTrait: - Sized + Clone + AnchorSerialize + AnchorDeserialize - { - /// The program ID that owns accounts of this variant type. - const PROGRAM_ID: Pubkey; - - /// The seeds struct type containing seed values. - type Seeds; - - /// The account data type. - type Data; - - /// The packed variant type for efficient serialization. - type Packed: PackedLightAccountVariantTrait; - - /// Get a reference to the account data. - fn data(&self) -> &Self::Data; - - /// Get seed values as owned byte vectors for PDA derivation. - fn seed_vec(&self) -> Vec>; - - /// Get seed references with bump for CPI signing. - /// Returns a fixed-size array that can be passed to invoke_signed. - fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; SEED_COUNT]; - - /// Derive the PDA address and bump seed using PROGRAM_ID. - fn derive_pda(&self) -> (Pubkey, u8) { - let seeds = self.seed_vec(); - let seed_slices: Vec<&[u8]> = seeds.iter().map(|s| s.as_slice()).collect(); - Pubkey::find_program_address(&seed_slices, &Self::PROGRAM_ID) - } - } - - /// Trait for packed compressed account variants. - /// - /// Packed variants use u8 indices instead of 32-byte Pubkeys for efficient - /// serialization. They can be unpacked back to full variants using account info. - #[allow(clippy::wrong_self_convention)] - pub trait PackedLightAccountVariantTrait: - Sized + Clone + AnchorSerialize + AnchorDeserialize - { - /// The unpacked variant type with full Pubkey values. - type Unpacked: LightAccountVariantTrait; - - /// The account type (Pda, Token, Ata, etc.) for dispatch. - const ACCOUNT_TYPE: AccountType; - - /// Get the PDA bump seed. - fn bump(&self) -> u8; - - /// Unpack this variant by resolving u8 indices to Pubkeys. - fn unpack(&self, accounts: &[AccountInfo]) -> Result; - - /// Get seed references with bump for CPI signing. - /// Resolves u8 indices to pubkey refs from accounts slice. - fn seed_refs_with_bump<'a>( - &'a self, - accounts: &'a [AccountInfo], - bump_storage: &'a [u8; 1], - ) -> std::result::Result<[&'a [u8]; SEED_COUNT], ProgramError>; - - /// Extract token data for compressed token CPI. - /// - /// Returns the packed token data needed for the token transfer instruction. - /// Only meaningful for token account variants; PDA variants should not override. - fn into_in_token_data( - &self, - tree_info: &PackedStateTreeInfo, - output_queue_index: u8, - ) -> Result; - - /// Extract TLV extension data for compressed token CPI. - /// - /// Returns extension instruction data if the token account has extensions. - /// Only meaningful for token account variants; PDA variants return `None`. - fn into_in_tlv(&self) -> Result>>; - - /// Derive the owner pubkey from constant owner_seeds and program ID. - /// Only meaningful for token account variants; PDA variants return default. - fn derive_owner(&self) -> Pubkey { - Pubkey::default() - } - } - - /// Trait for unpacked token seed structs. - /// - /// Generated by the `#[light_program]` macro on per-variant seed structs - /// (e.g., `TokenVaultSeeds`). Provides seed-specific behavior for the blanket - /// `LightAccountVariantTrait` impl on `TokenDataWithSeeds`. - pub trait UnpackedTokenSeeds: - Clone + std::fmt::Debug + AnchorSerialize + AnchorDeserialize - { - /// The packed seeds type. - type Packed: PackedTokenSeeds; - - const PROGRAM_ID: Pubkey; - fn seed_vec(&self) -> Vec>; - fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; N]; - } - - /// Trait for packed token seed structs. - /// - /// Generated by the `#[light_program]` macro on per-variant packed seed structs - /// (e.g., `PackedTokenVaultSeeds`). Provides seed-specific behavior for the blanket - /// `PackedLightAccountVariantTrait` impl on `TokenDataWithPackedSeeds`. - pub trait PackedTokenSeeds: - crate::Unpack + Clone + std::fmt::Debug + AnchorSerialize + AnchorDeserialize - { - fn bump(&self) -> u8; - fn seed_refs_with_bump<'a>( - &'a self, - accounts: &'a [AccountInfo], - bump_storage: &'a [u8; 1], - ) -> std::result::Result<[&'a [u8]; N], ProgramError>; - - /// Derive the owner pubkey from constant owner_seeds and program ID. - fn derive_owner(&self) -> Pubkey; - } -} - -#[cfg(feature = "anchor")] -pub use anchor_traits::{ - LightAccountVariantTrait, PackedLightAccountVariantTrait, PackedTokenSeeds, UnpackedTokenSeeds, -}; diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index d5c0dd19b4..346b6b3023 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -164,9 +164,6 @@ pub mod transfer; pub mod utils; pub use proof::borsh_compat; -pub mod interface; -/// Backward-compat alias -pub use interface as compressible; #[cfg(feature = "merkle-tree")] pub mod merkle_tree; @@ -203,15 +200,6 @@ pub mod sdk_types { use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; -// Pack trait is only available off-chain (client-side) - uses PackedAccounts -#[cfg(not(target_os = "solana"))] -pub use interface::Pack; -pub use interface::{ - process_initialize_light_config, process_initialize_light_config_checked, - process_update_light_config, CompressAs, CompressedInitSpace, CompressionInfo, - HasCompressionInfo, LightConfig, Space, Unpack, COMPRESSIBLE_CONFIG_SEED, - MAX_ADDRESS_TREES_PER_SPACE, -}; pub use light_account_checks::{self, discriminator::Discriminator as LightDiscriminator}; // Re-export as extern crate so downstream crates can use `::light_hasher::` paths pub extern crate light_hasher; @@ -224,11 +212,13 @@ pub use light_sdk_macros::{ }; pub use light_sdk_types::{constants, instruction::PackedAddressTreeInfoExt, CpiSigner}; use solana_account_info::AccountInfo; -use solana_cpi::invoke_signed; -use solana_instruction::{AccountMeta, Instruction}; +use solana_instruction::AccountMeta; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; +// Re-export SDK traits +pub use crate::cpi::LightCpiInstruction; + pub trait PubkeyTrait { fn to_solana_pubkey(&self) -> Pubkey; fn to_array(&self) -> [u8; 32]; diff --git a/sdk-libs/token-pinocchio/Cargo.toml b/sdk-libs/token-pinocchio/Cargo.toml new file mode 100644 index 0000000000..7bc9a7b5fd --- /dev/null +++ b/sdk-libs/token-pinocchio/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "light-token-pinocchio" +version = "0.4.0" +edition = { workspace = true } +description = "Pinocchio SDK for Light Tokens" +license = "Apache-2.0" +repository = "https://github.com/Lightprotocol/light-protocol" + +[features] +default = [] + +[dependencies] +# Pinocchio +pinocchio = { workspace = true } +pinocchio-pubkey = { workspace = true } + +# Light Protocol dependencies +light-sdk-types = { workspace = true, default-features = false, features = ["token", "v2", "alloc", "cpi-context"] } +light-token-interface = { workspace = true } +light-account-checks = { workspace = true, default-features = false, features = ["pinocchio"] } +light-compressed-account = { workspace = true } +light-macros = { workspace = true } + +# Serialization +borsh = { workspace = true } + +[dev-dependencies] +light-account-checks = { workspace = true, features = ["test-only", "pinocchio", "std", "solana"] } + +[lints.rust.unexpected_cfgs] +level = "allow" +check-cfg = [ + 'cfg(target_os, values("solana"))', + 'cfg(feature, values("frozen-abi", "no-entrypoint"))', +] diff --git a/sdk-libs/token-pinocchio/src/constants.rs b/sdk-libs/token-pinocchio/src/constants.rs new file mode 100644 index 0000000000..0999f6f950 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/constants.rs @@ -0,0 +1,38 @@ +//! Constants for Light Token Pinocchio SDK. +//! +//! Re-exports constants from `light_sdk_types::constants`. + +// Re-export core constants +pub use light_sdk_types::constants::{ + ACCOUNT_COMPRESSION_AUTHORITY_PDA, ACCOUNT_COMPRESSION_PROGRAM_ID, CPI_AUTHORITY_PDA_SEED, + LIGHT_SYSTEM_PROGRAM_ID, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_PROGRAM_ID, LIGHT_TOKEN_RENT_SPONSOR, + REGISTERED_PROGRAM_PDA, +}; + +/// CPI Authority PDA for the Light Token Program (as bytes) +pub const LIGHT_TOKEN_CPI_AUTHORITY: [u8; 32] = + light_macros::pubkey_array!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); + +/// Returns the program ID for the Light Token Program as bytes +#[inline] +pub const fn id() -> [u8; 32] { + LIGHT_TOKEN_PROGRAM_ID +} + +/// Return the CPI authority PDA of the Light Token Program as bytes. +#[inline] +pub const fn cpi_authority() -> [u8; 32] { + LIGHT_TOKEN_CPI_AUTHORITY +} + +/// Returns the default compressible config PDA as bytes. +#[inline] +pub const fn config_pda() -> [u8; 32] { + LIGHT_TOKEN_CONFIG +} + +/// Returns the default rent sponsor PDA as bytes. +#[inline] +pub const fn rent_sponsor_pda() -> [u8; 32] { + LIGHT_TOKEN_RENT_SPONSOR +} diff --git a/sdk-libs/token-pinocchio/src/error.rs b/sdk-libs/token-pinocchio/src/error.rs new file mode 100644 index 0000000000..e7686a48e2 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/error.rs @@ -0,0 +1,45 @@ +//! Error types for light-token-pinocchio SDK. + +use pinocchio::program_error::ProgramError; + +/// Result type for light-token-pinocchio specific errors +pub type LightTokenResult = core::result::Result; + +/// Errors specific to high-level token operations. +/// Error codes start at 17500 to avoid conflicts with other Light Protocol errors. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LightTokenError { + SplInterfaceRequired = 17500, + IncompleteSplInterface = 17501, + UseRegularSplTransfer = 17502, + CannotDetermineAccountType = 17503, + MissingMintAccount = 17504, + MissingSplTokenProgram = 17505, + MissingSplInterfacePda = 17506, + MissingSplInterfacePdaBump = 17507, + SplTokenProgramMismatch = 17508, + InvalidAccountData = 17509, + SerializationError = 17510, + MissingCpiContext = 17511, + MissingCpiAuthority = 17512, + MissingOutputQueue = 17513, + MissingStateMerkleTree = 17514, + MissingAddressMerkleTree = 17515, + MissingLightSystemProgram = 17516, + MissingRegisteredProgramPda = 17517, + MissingAccountCompressionAuthority = 17518, + MissingAccountCompressionProgram = 17519, + MissingSystemProgram = 17520, +} + +impl From for ProgramError { + fn from(e: LightTokenError) -> Self { + ProgramError::Custom(e as u32) + } +} + +impl From for u32 { + fn from(e: LightTokenError) -> Self { + e as u32 + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/approve.rs b/sdk-libs/token-pinocchio/src/instruction/approve.rs new file mode 100644 index 0000000000..af6f821271 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/approve.rs @@ -0,0 +1,76 @@ +//! Approve CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::constants::LIGHT_TOKEN_PROGRAM_ID; + +/// Approve ctoken via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::ApproveCpi; +/// +/// ApproveCpi { +/// token_account: &ctx.accounts.token_account, +/// delegate: &ctx.accounts.delegate, +/// owner: &ctx.accounts.owner, +/// system_program: &ctx.accounts.system_program, +/// amount: 100, +/// } +/// .invoke()?; +/// ``` +pub struct ApproveCpi<'info> { + pub token_account: &'info AccountInfo, + pub delegate: &'info AccountInfo, + pub owner: &'info AccountInfo, + pub system_program: &'info AccountInfo, + pub amount: u64, +} + +impl<'info> ApproveCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data: discriminator(1) + amount(8) + let mut data = [0u8; 9]; + data[0] = 4u8; // Approve discriminator + data[1..9].copy_from_slice(&self.amount.to_le_bytes()); + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + + let account_metas = [ + AccountMeta::writable(self.token_account.key()), + AccountMeta::readonly(self.delegate.key()), + AccountMeta::writable_signer(self.owner.key()), + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; + + let account_infos = [ + self.token_account, + self.delegate, + self.owner, + self.system_program, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/burn.rs b/sdk-libs/token-pinocchio/src/instruction/burn.rs new file mode 100644 index 0000000000..2bc9f423b0 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/burn.rs @@ -0,0 +1,117 @@ +//! Burn CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::constants::LIGHT_TOKEN_PROGRAM_ID; + +/// Burn ctoken via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::BurnCpi; +/// +/// BurnCpi { +/// source: &ctx.accounts.source, +/// mint: &ctx.accounts.mint, +/// amount: 100, +/// authority: &ctx.accounts.authority, +/// system_program: &ctx.accounts.system_program, +/// max_top_up: None, +/// fee_payer: None, +/// } +/// .invoke()?; +/// ``` +pub struct BurnCpi<'info> { + pub source: &'info AccountInfo, + pub mint: &'info AccountInfo, + pub amount: u64, + pub authority: &'info AccountInfo, + pub system_program: &'info AccountInfo, + /// Optional fee payer for rent top-ups. If not provided, authority pays. + pub fee_payer: Option<&'info AccountInfo>, +} + +impl<'info> BurnCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data: discriminator(1) + amount(8) + optional max_top_up(2) + let mut data = [0u8; 11]; + data[0] = 8u8; // Burn discriminator + data[1..9].copy_from_slice(&self.amount.to_le_bytes()); + let data_len = 9; + + // Authority is writable when no fee_payer is provided + let authority_writable = self.fee_payer.is_none(); + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + + if let Some(fee_payer) = self.fee_payer { + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::writable(self.mint.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(fee_payer.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [ + self.source, + self.mint, + self.authority, + self.system_program, + fee_payer, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } else { + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::writable(self.mint.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [self.source, self.mint, self.authority, self.system_program]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs b/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs new file mode 100644 index 0000000000..4793aaf1a8 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/burn_checked.rs @@ -0,0 +1,120 @@ +//! BurnChecked CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::constants::LIGHT_TOKEN_PROGRAM_ID; + +/// Burn ctoken checked via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::BurnCheckedCpi; +/// +/// BurnCheckedCpi { +/// source: &ctx.accounts.source, +/// mint: &ctx.accounts.mint, +/// amount: 100, +/// decimals: 9, +/// authority: &ctx.accounts.authority, +/// system_program: &ctx.accounts.system_program, +/// max_top_up: None, +/// fee_payer: None, +/// } +/// .invoke()?; +/// ``` +pub struct BurnCheckedCpi<'info> { + pub source: &'info AccountInfo, + pub mint: &'info AccountInfo, + pub amount: u64, + pub decimals: u8, + pub authority: &'info AccountInfo, + pub system_program: &'info AccountInfo, + /// Optional fee payer for rent top-ups. If not provided, authority pays. + pub fee_payer: Option<&'info AccountInfo>, +} + +impl<'info> BurnCheckedCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data: discriminator(1) + amount(8) + decimals(1) + optional max_top_up(2) + let mut data = [0u8; 12]; + data[0] = 15u8; // BurnChecked discriminator + data[1..9].copy_from_slice(&self.amount.to_le_bytes()); + data[9] = self.decimals; + let data_len = 10; + + // Authority is writable when no fee_payer is provided + let authority_writable = self.fee_payer.is_none(); + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + + if let Some(fee_payer) = self.fee_payer { + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::writable(self.mint.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(fee_payer.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [ + self.source, + self.mint, + self.authority, + self.system_program, + fee_payer, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } else { + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::writable(self.mint.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [self.source, self.mint, self.authority, self.system_program]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/close.rs b/sdk-libs/token-pinocchio/src/instruction/close.rs new file mode 100644 index 0000000000..66922f9d22 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/close.rs @@ -0,0 +1,77 @@ +//! Close CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +/// Close ctoken account via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::CloseAccountCpi; +/// +/// CloseAccountCpi { +/// token_program: &ctx.accounts.token_program, +/// account: &ctx.accounts.account, +/// destination: &ctx.accounts.destination, +/// owner: &ctx.accounts.owner, +/// rent_sponsor: &ctx.accounts.rent_sponsor, +/// } +/// .invoke()?; +/// ``` +pub struct CloseAccountCpi<'info> { + /// The token program to invoke (Light Token Program) + pub token_program: &'info AccountInfo, + /// The token account to close + pub account: &'info AccountInfo, + /// The destination to receive the account's lamports + pub destination: &'info AccountInfo, + /// The owner of the token account (signer) + pub owner: &'info AccountInfo, + /// The rent sponsor account + pub rent_sponsor: &'info AccountInfo, +} + +impl<'info> CloseAccountCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data: discriminator(1) only + let data = [9u8]; // Close discriminator + + let program_id = Pubkey::from(*self.token_program.key()); + + let account_metas = [ + AccountMeta::writable(self.account.key()), + AccountMeta::writable(self.destination.key()), + AccountMeta::writable_signer(self.owner.key()), + AccountMeta::writable(self.rent_sponsor.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; + + let account_infos = [ + self.account, + self.destination, + self.owner, + self.rent_sponsor, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/create.rs b/sdk-libs/token-pinocchio/src/instruction/create.rs new file mode 100644 index 0000000000..94a5430e4b --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/create.rs @@ -0,0 +1,9 @@ +//! Create CToken account CPI builder for pinocchio. +//! +//! Re-exports the generic `CreateTokenAccountCpi` from `light_sdk_types` +//! specialized for pinocchio's `AccountInfo`. + +// TODO: add types with generics set so that we dont expose the generics +pub use light_sdk_types::interface::cpi::create_token_accounts::{ + CreateTokenAccountCpi, CreateTokenAccountRentFreeCpi, +}; diff --git a/sdk-libs/token-pinocchio/src/instruction/create_ata.rs b/sdk-libs/token-pinocchio/src/instruction/create_ata.rs new file mode 100644 index 0000000000..3c96fea1f5 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/create_ata.rs @@ -0,0 +1,28 @@ +//! Create CToken ATA CPI builder for pinocchio. +//! +//! Re-exports the generic `CreateTokenAtaCpi` from `light_sdk_types` +//! specialized for pinocchio's `AccountInfo`. + +use light_account_checks::AccountInfoTrait; +// TODO: add types with generics set so that we dont expose the generics +pub use light_sdk_types::interface::cpi::create_token_accounts::{ + CreateTokenAtaCpi, CreateTokenAtaCpiIdempotent, CreateTokenAtaRentFreeCpi, +}; +use light_token_interface::LIGHT_TOKEN_PROGRAM_ID; +use pinocchio::account_info::AccountInfo; + +/// Derive the associated token account address for a given owner and mint. +/// +/// Returns `([u8; 32], u8)` -- the ATA address and bump seed. +/// +/// Uses pinocchio's `AccountInfo` for PDA derivation. +pub fn derive_associated_token_account(owner: &[u8; 32], mint: &[u8; 32]) -> ([u8; 32], u8) { + AccountInfo::find_program_address( + &[ + owner.as_ref(), + LIGHT_TOKEN_PROGRAM_ID.as_ref(), + mint.as_ref(), + ], + &LIGHT_TOKEN_PROGRAM_ID, + ) +} diff --git a/sdk-libs/token-pinocchio/src/instruction/create_mint.rs b/sdk-libs/token-pinocchio/src/instruction/create_mint.rs new file mode 100644 index 0000000000..6ec8281448 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/create_mint.rs @@ -0,0 +1,326 @@ +//! Create Light Mint CPI builder for pinocchio. +//! +//! Provides `CreateMintParams`, `CreateMintCpi`, and helper functions +//! for creating Light Mints via CPI from pinocchio-based programs. + +use alloc::{vec, vec::Vec}; + +use light_account_checks::AccountInfoTrait; +use light_compressed_account::instruction_data::{ + compressed_proof::CompressedProof, traits::LightInstructionData, +}; +use light_sdk_types::ADDRESS_TREE_V2; +use light_token_interface::{ + instructions::{ + extensions::ExtensionInstructionData, + mint_action::{CpiContext, DecompressMintAction, MintActionCompressedInstructionData}, + }, + state::MintMetadata, + COMPRESSED_MINT_SEED, +}; +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::{constants::LIGHT_TOKEN_PROGRAM_ID, instruction::SystemAccountInfos}; + +/// Parameters for creating a mint. +/// +/// Creates both a Light Mint AND a decompressed Mint Solana account +/// in a single instruction. +#[derive(Debug, Clone)] +pub struct CreateMintParams { + pub decimals: u8, + pub address_merkle_tree_root_index: u16, + pub mint_authority: [u8; 32], + pub proof: CompressedProof, + pub compression_address: [u8; 32], + pub mint: [u8; 32], + pub bump: u8, + pub freeze_authority: Option<[u8; 32]>, + pub extensions: Option>, + /// Rent payment in epochs for the Mint account (must be 0 or >= 2). + /// Default: 16 (~24 hours) + pub rent_payment: u8, + /// Lamports allocated for future write operations. + /// Default: 766 (~3 hours per write) + pub write_top_up: u32, +} + +impl Default for CreateMintParams { + fn default() -> Self { + Self { + decimals: 9, + address_merkle_tree_root_index: 0, + mint_authority: [0u8; 32], + proof: CompressedProof::default(), + compression_address: [0u8; 32], + mint: [0u8; 32], + bump: 0, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + } + } +} + +/// Create a mint via CPI. +/// +/// Creates both a Light Mint AND a decompressed Mint Solana account +/// in a single instruction. +/// +/// # Example +/// ```rust,ignore +/// CreateMintCpi { +/// mint_seed: &mint_seed_account, +/// authority: &authority_account, +/// payer: &payer_account, +/// address_tree: &address_tree_account, +/// output_queue: &output_queue_account, +/// compressible_config: &config_account, +/// mint: &mint_account, +/// rent_sponsor: &rent_sponsor_account, +/// system_accounts: &system_accounts, +/// cpi_context: None, +/// cpi_context_account: None, +/// params: CreateMintParams { +/// decimals: 9, +/// mint_authority: authority_pubkey, +/// // ... other params from validity proof +/// ..Default::default() +/// }, +/// } +/// .invoke()?; +/// ``` +pub struct CreateMintCpi<'info> { + /// Used as seed for the mint address (must be a signer). + pub mint_seed: &'info AccountInfo, + /// The authority for the mint (will be stored as mint_authority). + pub authority: &'info AccountInfo, + /// The fee payer for the transaction. + pub payer: &'info AccountInfo, + pub address_tree: &'info AccountInfo, + pub output_queue: &'info AccountInfo, + /// CompressibleConfig account (required for Mint creation) + pub compressible_config: &'info AccountInfo, + /// Mint PDA account (writable, will be initialized) + pub mint: &'info AccountInfo, + /// Rent sponsor PDA (required for Mint creation) + pub rent_sponsor: &'info AccountInfo, + pub system_accounts: SystemAccountInfos<'info>, + pub cpi_context: Option, + pub cpi_context_account: Option<&'info AccountInfo>, + pub params: CreateMintParams, +} + +impl<'info> CreateMintCpi<'info> { + #[allow(clippy::too_many_arguments)] + pub fn new( + mint_seed: &'info AccountInfo, + authority: &'info AccountInfo, + payer: &'info AccountInfo, + address_tree: &'info AccountInfo, + output_queue: &'info AccountInfo, + compressible_config: &'info AccountInfo, + mint: &'info AccountInfo, + rent_sponsor: &'info AccountInfo, + system_accounts: SystemAccountInfos<'info>, + params: CreateMintParams, + ) -> Self { + Self { + mint_seed, + authority, + payer, + address_tree, + output_queue, + compressible_config, + mint, + rent_sponsor, + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, + } + } + + pub fn with_cpi_context( + mut self, + cpi_context: CpiContext, + cpi_context_account: &'info AccountInfo, + ) -> Self { + self.cpi_context = Some(cpi_context); + self.cpi_context_account = Some(cpi_context_account); + self + } + + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + let (ix_data, account_metas, account_infos) = self.build_instruction_inner()?; + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &ix_data, + }; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } + + #[allow(clippy::type_complexity)] + fn build_instruction_inner( + &self, + ) -> Result<(Vec, Vec>, Vec<&AccountInfo>), ProgramError> { + // Validate mint_authority matches authority account + if self.params.mint_authority != *self.authority.key() { + return Err(ProgramError::InvalidAccountData); + } + + // Build MintInstructionData + let mint_instruction_data = + light_token_interface::instructions::mint_action::MintInstructionData { + supply: 0, + decimals: self.params.decimals, + metadata: MintMetadata { + version: 3, + mint: self.params.mint.into(), + mint_decompressed: false, + mint_signer: *self.mint_seed.key(), + bump: self.params.bump, + }, + mint_authority: Some(self.params.mint_authority.into()), + freeze_authority: self.params.freeze_authority.map(|auth| auth.into()), + extensions: self.params.extensions.clone(), + }; + + // Build instruction data + let mut instruction_data = MintActionCompressedInstructionData::new_mint( + self.params.address_merkle_tree_root_index, + self.params.proof, + mint_instruction_data, + ); + + // Always add decompress action to create Mint Solana account + instruction_data = instruction_data.with_decompress_mint(DecompressMintAction { + rent_payment: self.params.rent_payment, + write_top_up: self.params.write_top_up, + }); + + if let Some(ctx) = &self.cpi_context { + instruction_data = instruction_data.with_cpi_context(ctx.clone()); + } + + let ix_data = instruction_data + .data() + .map_err(|_| ProgramError::BorshIoError)?; + + // Build account metas and account infos in matching order + // Order matches MintActionMetaConfig::to_account_metas: + // 1. light_system_program + // 2. mint_seed (signer) + // 3. authority (signer) + // 4. compressible_config + // 5. mint (writable) + // 6. rent_sponsor (writable) + // 7. fee_payer (signer, writable) + // 8. cpi_authority_pda + // 9. registered_program_pda + // 10. account_compression_authority + // 11. account_compression_program + // 12. system_program + // [optional: cpi_context_account] + // 13. output_queue (writable) + // 14. address_tree (writable) + + let mut account_metas = vec![ + AccountMeta::readonly(self.system_accounts.light_system_program.key()), + AccountMeta::readonly_signer(self.mint_seed.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::readonly(self.compressible_config.key()), + AccountMeta::writable(self.mint.key()), + AccountMeta::writable(self.rent_sponsor.key()), + AccountMeta::writable_signer(self.payer.key()), + AccountMeta::readonly(self.system_accounts.cpi_authority_pda.key()), + AccountMeta::readonly(self.system_accounts.registered_program_pda.key()), + AccountMeta::readonly(self.system_accounts.account_compression_authority.key()), + AccountMeta::readonly(self.system_accounts.account_compression_program.key()), + AccountMeta::readonly(self.system_accounts.system_program.key()), + ]; + + let mut account_infos = vec![ + self.system_accounts.light_system_program, + self.mint_seed, + self.authority, + self.compressible_config, + self.mint, + self.rent_sponsor, + self.payer, + self.system_accounts.cpi_authority_pda, + self.system_accounts.registered_program_pda, + self.system_accounts.account_compression_authority, + self.system_accounts.account_compression_program, + self.system_accounts.system_program, + ]; + + // Add optional cpi_context_account + if let Some(cpi_ctx_acc) = self.cpi_context_account { + account_metas.push(AccountMeta::writable(cpi_ctx_acc.key())); + account_infos.push(cpi_ctx_acc); + } + + // Add output_queue and address_tree + account_metas.push(AccountMeta::writable(self.output_queue.key())); + account_metas.push(AccountMeta::writable(self.address_tree.key())); + + account_infos.push(self.output_queue); + account_infos.push(self.address_tree); + + Ok((ix_data, account_metas, account_infos)) + } +} + +// ============================================================================ +// Helper Functions +// ============================================================================ + +/// Derives the Light Mint address from the mint seed and address tree. +pub fn derive_mint_compressed_address( + mint_seed: &[u8; 32], + address_tree_pubkey: &[u8; 32], +) -> [u8; 32] { + let (mint_pda, _) = find_mint_address(mint_seed); + light_compressed_account::address::derive_address( + &mint_pda, + address_tree_pubkey, + &LIGHT_TOKEN_PROGRAM_ID, + ) +} + +/// Derives the compressed address from a Light mint address. +pub fn derive_compressed_address(mint: &[u8; 32]) -> [u8; 32] { + light_compressed_account::address::derive_address( + mint, + &ADDRESS_TREE_V2, + &LIGHT_TOKEN_PROGRAM_ID, + ) +} + +/// Finds the compressed mint PDA address from a mint seed. +pub fn find_mint_address(mint_seed: &[u8; 32]) -> ([u8; 32], u8) { + AccountInfo::find_program_address( + &[COMPRESSED_MINT_SEED, mint_seed.as_ref()], + &LIGHT_TOKEN_PROGRAM_ID, + ) +} diff --git a/sdk-libs/token-pinocchio/src/instruction/create_mints.rs b/sdk-libs/token-pinocchio/src/instruction/create_mints.rs new file mode 100644 index 0000000000..c246d49f3c --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/create_mints.rs @@ -0,0 +1,124 @@ +//! Batch create Light Mints CPI for pinocchio. +//! +//! Provides type aliases with pinocchio's `AccountInfo` already set, +//! wrapping the generic types from `light_sdk_types`. +//! +//! # Flow +//! +//! - N=1 (no CPI context offset): Single CPI (create + decompress) +//! - N>1 or offset>0: 2N-1 CPIs (N-1 writes + 1 execute with decompress + N-1 decompress) +//! +//! # Example +//! +//! ```rust,ignore +//! use light_token_pinocchio::instruction::{ +//! CreateMintsCpi, CreateMintsParams, SingleMintParams, +//! get_output_queue_next_index, +//! }; +//! +//! // Get base leaf index before any CPIs +//! let base_leaf_index = get_output_queue_next_index(&output_queue)?; +//! +//! // mint and compression_address are derived internally from mint_seed_pubkey +//! let mint_params = [SingleMintParams { +//! decimals: 9, +//! address_merkle_tree_root_index: root_index, +//! mint_authority: authority_key, +//! mint_bump: None, // derived from mint_seed_pubkey if None +//! freeze_authority: None, +//! mint_seed_pubkey: mint_seed_key, +//! authority_seeds: None, +//! mint_signer_seeds: Some(&mint_signer_seeds), +//! token_metadata: None, +//! }]; +//! +//! let params = CreateMintsParams { +//! mints: &mint_params, +//! proof, +//! rent_payment: 16, +//! write_top_up: 766, +//! cpi_context_offset: 0, +//! output_queue_index: 0, +//! address_tree_index: 1, +//! state_tree_index: 2, +//! base_leaf_index, +//! }; +//! +//! CreateMintsCpi { +//! mint_seed_accounts: &[&mint_seed_account], +//! payer: &payer, +//! address_tree: &address_tree, +//! output_queue: &output_queue, +//! state_merkle_tree: &state_merkle_tree, +//! compressible_config: &config, +//! mints: &[&mint_pda_account], +//! rent_sponsor: &rent_sponsor, +//! light_system_program: &light_system, +//! cpi_authority_pda: &cpi_authority, +//! registered_program_pda: ®istered_program, +//! account_compression_authority: &compression_authority, +//! account_compression_program: &compression_program, +//! system_program: &system_program, +//! cpi_context_account: &cpi_context, +//! params, +//! }.invoke()?; +//! ``` + +// Re-export non-generic types and functions directly +pub use light_sdk_types::interface::cpi::create_mints::{ + get_output_queue_next_index, CreateMintsParams, SingleMintParams, DEFAULT_RENT_PAYMENT, + DEFAULT_WRITE_TOP_UP, +}; +use pinocchio::account_info::AccountInfo; + +/// High-level struct for creating compressed mints (pinocchio). +/// +/// Type alias with pinocchio's `AccountInfo` already set. +/// Consolidates proof parsing, tree account resolution, and CPI invocation. +/// +/// # Example +/// +/// ```rust,ignore +/// CreateMints { +/// mints: &sdk_mints, +/// proof_data: ¶ms.create_accounts_proof, +/// mint_seed_accounts, +/// mint_accounts, +/// infra, +/// } +/// .invoke(&cpi_accounts)?; +/// ``` +pub type CreateMints<'a> = + light_sdk_types::interface::cpi::create_mints::CreateMints<'a, AccountInfo>; + +/// CPI struct for creating multiple Light Mints (pinocchio). +/// +/// Type alias with pinocchio's `AccountInfo` already set. +pub type CreateMintsCpi<'a> = + light_sdk_types::interface::cpi::create_mints::CreateMintsCpi<'a, AccountInfo>; + +/// Infrastructure accounts for mint creation CPI (pinocchio). +/// +/// Type alias with pinocchio's `AccountInfo` already set. +pub type CreateMintsStaticAccounts<'a> = + light_sdk_types::interface::cpi::create_mints::CreateMintsStaticAccounts<'a, AccountInfo>; + +/// Find the mint PDA address for a given mint seed (pinocchio). +/// +/// Returns `([u8; 32], u8)` -- the PDA address bytes and bump. +pub fn find_mint_address(mint_seed: &[u8; 32]) -> ([u8; 32], u8) { + light_sdk_types::interface::cpi::create_mints::find_mint_address::(mint_seed) +} + +/// Derive the Light Mint address from a mint seed and address tree pubkey (pinocchio). +/// +/// This computes `derive_address(find_mint_address(mint_seed).0, address_tree, LIGHT_TOKEN_PROGRAM_ID)`. +pub fn derive_mint_compressed_address( + mint_seed: &[u8; 32], + address_tree_pubkey: &[u8; 32], +) -> [u8; 32] { + light_sdk_types::interface::cpi::create_mints::derive_mint_compressed_address::( + mint_seed, + address_tree_pubkey, + ) +} diff --git a/sdk-libs/token-pinocchio/src/instruction/freeze.rs b/sdk-libs/token-pinocchio/src/instruction/freeze.rs new file mode 100644 index 0000000000..22b845d21e --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/freeze.rs @@ -0,0 +1,64 @@ +//! Freeze CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::constants::LIGHT_TOKEN_PROGRAM_ID; + +/// Freeze ctoken via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::FreezeCpi; +/// +/// FreezeCpi { +/// token_account: &ctx.accounts.token_account, +/// mint: &ctx.accounts.mint, +/// freeze_authority: &ctx.accounts.freeze_authority, +/// } +/// .invoke()?; +/// ``` +pub struct FreezeCpi<'info> { + pub token_account: &'info AccountInfo, + pub mint: &'info AccountInfo, + pub freeze_authority: &'info AccountInfo, +} + +impl<'info> FreezeCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data: discriminator(1) only + let data = [10u8]; // Freeze discriminator + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + + let account_metas = [ + AccountMeta::writable(self.token_account.key()), + AccountMeta::readonly(self.mint.key()), + AccountMeta::readonly_signer(self.freeze_authority.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; + + let account_infos = [self.token_account, self.mint, self.freeze_authority]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/mint_to.rs b/sdk-libs/token-pinocchio/src/instruction/mint_to.rs new file mode 100644 index 0000000000..7ca415540a --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/mint_to.rs @@ -0,0 +1,122 @@ +//! MintTo CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::constants::LIGHT_TOKEN_PROGRAM_ID; + +/// Mint to ctoken via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::MintToCpi; +/// +/// MintToCpi { +/// mint: &ctx.accounts.mint, +/// destination: &ctx.accounts.destination, +/// amount: 100, +/// authority: &ctx.accounts.authority, +/// system_program: &ctx.accounts.system_program, +/// max_top_up: None, +/// fee_payer: None, +/// } +/// .invoke()?; +/// ``` +pub struct MintToCpi<'info> { + pub mint: &'info AccountInfo, + pub destination: &'info AccountInfo, + pub amount: u64, + pub authority: &'info AccountInfo, + pub system_program: &'info AccountInfo, + /// Optional fee payer for rent top-ups. If not provided, authority pays. + pub fee_payer: Option<&'info AccountInfo>, +} + +impl<'info> MintToCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data: discriminator(1) + amount(8) + optional max_top_up(2) + let mut data = [0u8; 11]; + data[0] = 7u8; // MintTo discriminator + data[1..9].copy_from_slice(&self.amount.to_le_bytes()); + let data_len = 9; + + // Authority is writable when no fee_payer is provided + let authority_writable = self.fee_payer.is_none(); + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + + if let Some(fee_payer) = self.fee_payer { + let account_metas = [ + AccountMeta::writable(self.mint.key()), + AccountMeta::writable(self.destination.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(fee_payer.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [ + self.mint, + self.destination, + self.authority, + self.system_program, + fee_payer, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } else { + let account_metas = [ + AccountMeta::writable(self.mint.key()), + AccountMeta::writable(self.destination.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [ + self.mint, + self.destination, + self.authority, + self.system_program, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs b/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs new file mode 100644 index 0000000000..38e0dd8688 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/mint_to_checked.rs @@ -0,0 +1,125 @@ +//! MintToChecked CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::constants::LIGHT_TOKEN_PROGRAM_ID; + +/// Mint to ctoken checked via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::MintToCheckedCpi; +/// +/// MintToCheckedCpi { +/// mint: &ctx.accounts.mint, +/// destination: &ctx.accounts.destination, +/// amount: 100, +/// decimals: 9, +/// authority: &ctx.accounts.authority, +/// system_program: &ctx.accounts.system_program, +/// max_top_up: None, +/// fee_payer: None, +/// } +/// .invoke()?; +/// ``` +pub struct MintToCheckedCpi<'info> { + pub mint: &'info AccountInfo, + pub destination: &'info AccountInfo, + pub amount: u64, + pub decimals: u8, + pub authority: &'info AccountInfo, + pub system_program: &'info AccountInfo, + /// Optional fee payer for rent top-ups. If not provided, authority pays. + pub fee_payer: Option<&'info AccountInfo>, +} + +impl<'info> MintToCheckedCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data: discriminator(1) + amount(8) + decimals(1) + optional max_top_up(2) + let mut data = [0u8; 12]; + data[0] = 14u8; // MintToChecked discriminator + data[1..9].copy_from_slice(&self.amount.to_le_bytes()); + data[9] = self.decimals; + let data_len = 10; + + // Authority is writable when no fee_payer is provided + let authority_writable = self.fee_payer.is_none(); + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + + if let Some(fee_payer) = self.fee_payer { + let account_metas = [ + AccountMeta::writable(self.mint.key()), + AccountMeta::writable(self.destination.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(fee_payer.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [ + self.mint, + self.destination, + self.authority, + self.system_program, + fee_payer, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } else { + let account_metas = [ + AccountMeta::writable(self.mint.key()), + AccountMeta::writable(self.destination.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [ + self.mint, + self.destination, + self.authority, + self.system_program, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/mod.rs b/sdk-libs/token-pinocchio/src/instruction/mod.rs new file mode 100644 index 0000000000..5736746fbd --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/mod.rs @@ -0,0 +1,109 @@ +//! CPI builders for Light Token operations with Pinocchio. +//! +//! This module provides CPI (Cross-Program Invocation) builders that use +//! Pinocchio types for efficient on-chain token operations. +//! +//! ## Account Creation +//! +//! - [`CreateTokenAccountCpi`] - Create Light Token account via CPI +//! - [`CreateTokenAtaCpi`] - Create associated Light Token account (ATA) via CPI +//! +//! ## Transfers +//! +//! - [`TransferCpi`] - Transfer between Light Token accounts +//! - [`TransferFromSplCpi`] - Transfer from SPL token account to Light Token account +//! - [`TransferToSplCpi`] - Transfer from Light Token account to SPL token account +//! - [`TransferInterfaceCpi`] - Transfer via CPI, auto-detect source/destination account types +//! +//! ## Mint Operations +//! +//! - [`CreateMintCpi`] - Create single Light Mint via CPI +//! - [`CreateMintsCpi`] - Batch create Light Mints via CPI +//! - [`MintToCpi`] - Mint tokens to Light Token accounts +//! +//! ## Other Operations +//! +//! - [`ApproveCpi`] - Approve delegation +//! - [`RevokeCpi`] - Revoke delegation +//! - [`FreezeCpi`] - Freeze account +//! - [`ThawCpi`] - Thaw frozen account +//! - [`BurnCpi`] - Burn tokens +//! - [`CloseAccountCpi`] - Close Light Token account + +mod approve; +mod burn; +mod burn_checked; +mod close; +mod create; +mod create_ata; +mod create_mint; +mod create_mints; +mod freeze; +mod mint_to; +mod mint_to_checked; +mod revoke; +mod thaw; +mod transfer; +mod transfer_checked; +mod transfer_from_spl; +mod transfer_interface; +mod transfer_to_spl; + +pub use approve::*; +pub use burn::*; +pub use burn_checked::*; +pub use close::*; +pub use create::*; +pub use create_ata::{ + derive_associated_token_account, CreateTokenAtaCpi, CreateTokenAtaCpiIdempotent, + CreateTokenAtaRentFreeCpi, +}; +pub use create_mint::*; +pub use create_mints::{ + derive_mint_compressed_address as derive_mint_compressed_address_batch, + find_mint_address as find_mint_address_batch, get_output_queue_next_index, CreateMints, + CreateMintsCpi, CreateMintsParams, CreateMintsStaticAccounts, SingleMintParams, + DEFAULT_RENT_PAYMENT, DEFAULT_WRITE_TOP_UP, +}; +pub use freeze::*; +pub use light_compressed_account::instruction_data::compressed_proof::{ + CompressedProof, ValidityProof, +}; +pub use light_token_interface::{ + instructions::{ + extensions::{CompressToPubkey, ExtensionInstructionData, TokenMetadataInstructionData}, + mint_action::MintWithContext, + }, + state::{AdditionalMetadata, Token}, +}; +pub use mint_to::*; +pub use mint_to_checked::*; +use pinocchio::account_info::AccountInfo; +pub use revoke::*; +pub use thaw::*; +pub use transfer::*; +pub use transfer_checked::*; +pub use transfer_from_spl::TransferFromSplCpi; +pub use transfer_interface::{SplInterfaceCpi, TransferInterfaceCpi}; +pub use transfer_to_spl::TransferToSplCpi; +/// System accounts required for CPI operations to Light Protocol. +/// +/// Pass these accounts when invoking Light Token operations from your program. +/// +/// # Fields +/// +/// - `light_system_program` - Light System Program +/// - `cpi_authority_pda` - CPI authority (signs for your program) +/// - `registered_program_pda` - Your program's registration +/// - `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: &'info AccountInfo, + pub cpi_authority_pda: &'info AccountInfo, + pub registered_program_pda: &'info AccountInfo, + pub account_compression_authority: &'info AccountInfo, + pub account_compression_program: &'info AccountInfo, + pub system_program: &'info AccountInfo, +} diff --git a/sdk-libs/token-pinocchio/src/instruction/revoke.rs b/sdk-libs/token-pinocchio/src/instruction/revoke.rs new file mode 100644 index 0000000000..1cbca61334 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/revoke.rs @@ -0,0 +1,64 @@ +//! Revoke CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::constants::LIGHT_TOKEN_PROGRAM_ID; + +/// Revoke ctoken via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::RevokeCpi; +/// +/// RevokeCpi { +/// token_account: &ctx.accounts.token_account, +/// owner: &ctx.accounts.owner, +/// system_program: &ctx.accounts.system_program, +/// } +/// .invoke()?; +/// ``` +pub struct RevokeCpi<'info> { + pub token_account: &'info AccountInfo, + pub owner: &'info AccountInfo, + pub system_program: &'info AccountInfo, +} + +impl<'info> RevokeCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data: discriminator(1) only + let data = [5u8]; // Revoke discriminator + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + + let account_metas = [ + AccountMeta::writable(self.token_account.key()), + AccountMeta::writable_signer(self.owner.key()), + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; + + let account_infos = [self.token_account, self.owner, self.system_program]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/thaw.rs b/sdk-libs/token-pinocchio/src/instruction/thaw.rs new file mode 100644 index 0000000000..76d40d2b06 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/thaw.rs @@ -0,0 +1,64 @@ +//! Thaw CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::constants::LIGHT_TOKEN_PROGRAM_ID; + +/// Thaw ctoken via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::ThawCpi; +/// +/// ThawCpi { +/// token_account: &ctx.accounts.token_account, +/// mint: &ctx.accounts.mint, +/// freeze_authority: &ctx.accounts.freeze_authority, +/// } +/// .invoke()?; +/// ``` +pub struct ThawCpi<'info> { + pub token_account: &'info AccountInfo, + pub mint: &'info AccountInfo, + pub freeze_authority: &'info AccountInfo, +} + +impl<'info> ThawCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data: discriminator(1) only + let data = [11u8]; // Thaw discriminator + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + + let account_metas = [ + AccountMeta::writable(self.token_account.key()), + AccountMeta::readonly(self.mint.key()), + AccountMeta::readonly_signer(self.freeze_authority.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data, + }; + + let account_infos = [self.token_account, self.mint, self.freeze_authority]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer.rs b/sdk-libs/token-pinocchio/src/instruction/transfer.rs new file mode 100644 index 0000000000..b27b3ff013 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/transfer.rs @@ -0,0 +1,122 @@ +//! Transfer CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::constants::LIGHT_TOKEN_PROGRAM_ID; + +/// Transfer ctoken via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::TransferCpi; +/// +/// TransferCpi { +/// source: &ctx.accounts.source, +/// destination: &ctx.accounts.destination, +/// amount: 100, +/// authority: &ctx.accounts.authority, +/// system_program: &ctx.accounts.system_program, +/// max_top_up: None, +/// fee_payer: None, +/// } +/// .invoke()?; +/// ``` +pub struct TransferCpi<'info> { + pub source: &'info AccountInfo, + pub destination: &'info AccountInfo, + pub amount: u64, + pub authority: &'info AccountInfo, + pub system_program: &'info AccountInfo, + /// Optional fee payer for rent top-ups. If not provided, authority pays. + pub fee_payer: Option<&'info AccountInfo>, +} + +impl<'info> TransferCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data + let mut data = [0u8; 11]; // discriminator(1) + amount(8) + optional max_top_up(2) + data[0] = 3u8; // Transfer discriminator + data[1..9].copy_from_slice(&self.amount.to_le_bytes()); + let data_len = 9; + + // Authority is writable when no fee_payer is provided + let authority_writable = self.fee_payer.is_none(); + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + + if let Some(fee_payer) = self.fee_payer { + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::writable(self.destination.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(fee_payer.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [ + self.source, + self.destination, + self.authority, + self.system_program, + fee_payer, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } else { + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::writable(self.destination.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [ + self.source, + self.destination, + self.authority, + self.system_program, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs new file mode 100644 index 0000000000..f3b7e8f229 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_checked.rs @@ -0,0 +1,130 @@ +//! TransferChecked CPI for Light Token operations. + +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +use crate::constants::LIGHT_TOKEN_PROGRAM_ID; + +/// Transfer ctoken checked via CPI. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_pinocchio::instruction::TransferCheckedCpi; +/// +/// TransferCheckedCpi { +/// source: &ctx.accounts.source, +/// mint: &ctx.accounts.mint, +/// destination: &ctx.accounts.destination, +/// amount: 100, +/// decimals: 9, +/// authority: &ctx.accounts.authority, +/// system_program: &ctx.accounts.system_program, +/// fee_payer: None, +/// } +/// .invoke()?; +/// ``` +pub struct TransferCheckedCpi<'info> { + pub source: &'info AccountInfo, + pub mint: &'info AccountInfo, + pub destination: &'info AccountInfo, + pub amount: u64, + pub decimals: u8, + pub authority: &'info AccountInfo, + pub system_program: &'info AccountInfo, + /// Optional fee payer for rent top-ups. If not provided, authority pays. + pub fee_payer: Option<&'info AccountInfo>, +} + +impl<'info> TransferCheckedCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + // Build instruction data: discriminator(1) + amount(8) + decimals(1) + optional max_top_up(2) + let mut data = [0u8; 12]; + data[0] = 12u8; // TransferChecked discriminator + data[1..9].copy_from_slice(&self.amount.to_le_bytes()); + data[9] = self.decimals; + let data_len = 10; + + // Authority is writable only when no fee_payer + let authority_writable = self.fee_payer.is_none(); + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + + if let Some(fee_payer) = self.fee_payer { + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::readonly(self.mint.key()), + AccountMeta::writable(self.destination.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + AccountMeta::writable_signer(fee_payer.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [ + self.source, + self.mint, + self.destination, + self.authority, + self.system_program, + fee_payer, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } else { + let account_metas = [ + AccountMeta::writable(self.source.key()), + AccountMeta::readonly(self.mint.key()), + AccountMeta::writable(self.destination.key()), + if authority_writable { + AccountMeta::writable_signer(self.authority.key()) + } else { + AccountMeta::readonly_signer(self.authority.key()) + }, + AccountMeta::readonly(self.system_program.key()), + ]; + + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &data[..data_len], + }; + + let account_infos = [ + self.source, + self.mint, + self.destination, + self.authority, + self.system_program, + ]; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_from_spl.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_from_spl.rs new file mode 100644 index 0000000000..00c7b51060 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_from_spl.rs @@ -0,0 +1,168 @@ +//! Transfer from SPL token account to CToken account via CPI. + +use alloc::{vec, vec::Vec}; + +use borsh::BorshSerialize; +use light_token_interface::{ + instructions::transfer2::{CompressedTokenInstructionDataTransfer2, Compression}, + LIGHT_TOKEN_PROGRAM_ID, +}; +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +/// Discriminator for Transfer2 instruction +const TRANSFER2_DISCRIMINATOR: u8 = 101; + +/// Transfer from SPL token account to CToken account via CPI. +/// +/// # Example +/// ```rust,ignore +/// TransferFromSplCpi { +/// amount: 100, +/// spl_interface_pda_bump: 255, +/// decimals: 9, +/// source_spl_token_account: &source, +/// destination: &destination, +/// authority: &authority, +/// mint: &mint, +/// payer: &payer, +/// spl_interface_pda: &spl_interface, +/// spl_token_program: &spl_token, +/// compressed_token_program_authority: &cpi_authority, +/// system_program: &system, +/// } +/// .invoke()?; +/// ``` +pub struct TransferFromSplCpi<'info> { + pub amount: u64, + pub spl_interface_pda_bump: u8, + pub decimals: u8, + pub source_spl_token_account: &'info AccountInfo, + /// Destination ctoken account (writable) + pub destination: &'info AccountInfo, + pub authority: &'info AccountInfo, + pub mint: &'info AccountInfo, + pub payer: &'info AccountInfo, + pub spl_interface_pda: &'info AccountInfo, + pub spl_token_program: &'info AccountInfo, + pub compressed_token_program_authority: &'info AccountInfo, + /// System program - required for compressible account lamport top-ups + pub system_program: &'info AccountInfo, +} + +impl<'info> TransferFromSplCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + let (ix_data, account_metas, account_infos) = self.build_instruction_inner()?; + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &ix_data, + }; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } + + #[allow(clippy::type_complexity)] + fn build_instruction_inner( + &self, + ) -> Result<(Vec, Vec>, Vec<&AccountInfo>), ProgramError> { + // Build compressions: + // 1. Wrap SPL tokens to Light Token pool + // 2. Unwrap from pool to destination ctoken account + let wrap_from_spl = Compression::compress_spl( + self.amount, + 0, // mint index + 3, // source index + 2, // authority index + 4, // pool_account_index + 0, // pool_index + self.spl_interface_pda_bump, + self.decimals, + ); + + let unwrap_to_destination = Compression::decompress( + self.amount, + 0, // mint index + 1, // destination index + ); + + // Build instruction data + // Note: out_token_data must be empty for compressions-only (Path A) operations. + // The program determines the path based on: no_compressed_accounts = in_token_data.is_empty() && out_token_data.is_empty() + // If out_token_data is non-empty, the program expects Path B accounts (with light_system_program, registered_program_pda, etc.) + let instruction_data = CompressedTokenInstructionDataTransfer2 { + with_transaction_hash: false, + with_lamports_change_account_merkle_tree_index: false, + lamports_change_account_merkle_tree_index: 0, + lamports_change_account_owner_index: 0, + output_queue: 0, + max_top_up: 0, + cpi_context: None, + compressions: Some(vec![wrap_from_spl, unwrap_to_destination]), + proof: None, + in_token_data: vec![], + out_token_data: vec![], + in_lamports: None, + out_lamports: None, + in_tlv: None, + out_tlv: None, + }; + + let mut ix_data = vec![TRANSFER2_DISCRIMINATOR]; + instruction_data + .serialize(&mut ix_data) + .map_err(|_| ProgramError::BorshIoError)?; + + // Build account metas matching transfer2 format: + // [0] cpi_authority_pda (readonly) + // [1] fee_payer (signer, writable) + // [2..] packed_accounts: + // - [0] mint (readonly) + // - [1] destination ctoken account (writable) + // - [2] authority (signer, readonly) + // - [3] source SPL token account (writable) + // - [4] SPL interface PDA (writable) + // - [5] SPL Token program (readonly) + // - [6] System program (readonly) + let account_metas = vec![ + AccountMeta::readonly(self.compressed_token_program_authority.key()), + AccountMeta::writable_signer(self.payer.key()), + AccountMeta::readonly(self.mint.key()), + AccountMeta::writable(self.destination.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::writable(self.source_spl_token_account.key()), + AccountMeta::writable(self.spl_interface_pda.key()), + AccountMeta::readonly(self.spl_token_program.key()), + AccountMeta::readonly(self.system_program.key()), + ]; + + let account_infos = vec![ + self.compressed_token_program_authority, + self.payer, + self.mint, + self.destination, + self.authority, + self.source_spl_token_account, + self.spl_interface_pda, + self.spl_token_program, + self.system_program, + ]; + + Ok((ix_data, account_metas, account_infos)) + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs new file mode 100644 index 0000000000..e7f0e0f085 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_interface.rs @@ -0,0 +1,312 @@ +//! Unified transfer interface that auto-routes based on account types. + +use light_token_interface::LIGHT_TOKEN_PROGRAM_ID; +use pinocchio::{ + account_info::AccountInfo, + cpi::{invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, +}; + +use super::{ + transfer::TransferCpi, transfer_from_spl::TransferFromSplCpi, transfer_to_spl::TransferToSplCpi, +}; + +/// SPL Token transfer_checked instruction discriminator +const SPL_TRANSFER_CHECKED_DISCRIMINATOR: u8 = 12; + +/// Check if an account is owned by the Light Token program. +fn is_light_token_owner(owner: &[u8; 32]) -> bool { + owner == &LIGHT_TOKEN_PROGRAM_ID +} + +/// Internal enum to classify transfer types based on account owners. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum TransferType { + /// light -> light + LightToLight, + /// light -> SPL (decompress) + LightToSpl, + /// SPL -> light (compress) + SplToLight, + /// SPL -> SPL (pass-through) + SplToSpl, +} + +/// Determine transfer type from account owners. +fn determine_transfer_type(source_owner: &[u8; 32], destination_owner: &[u8; 32]) -> TransferType { + let source_is_light = is_light_token_owner(source_owner); + let dest_is_light = is_light_token_owner(destination_owner); + + match (source_is_light, dest_is_light) { + (true, true) => TransferType::LightToLight, + (true, false) => TransferType::LightToSpl, + (false, true) => TransferType::SplToLight, + (false, false) => TransferType::SplToSpl, + } +} + +/// Required accounts to interface between light and SPL token accounts. +pub struct SplInterfaceCpi<'info> { + pub mint: &'info AccountInfo, + pub spl_token_program: &'info AccountInfo, + pub spl_interface_pda: &'info AccountInfo, + pub spl_interface_pda_bump: u8, +} + +/// Unified transfer interface that auto-routes based on account types. +/// +/// Detects whether source and destination are Light token accounts or SPL token +/// accounts and routes to the appropriate transfer implementation. +/// +/// # Example +/// ```rust,ignore +/// TransferInterfaceCpi::new( +/// 100, // amount +/// 9, // decimals +/// &source_account, +/// &destination_account, +/// &authority, +/// &payer, +/// &compressed_token_program_authority, +/// &system_program, +/// ) +/// .invoke()?; +/// ``` +pub struct TransferInterfaceCpi<'info> { + pub amount: u64, + pub decimals: u8, + pub source_account: &'info AccountInfo, + pub destination_account: &'info AccountInfo, + pub authority: &'info AccountInfo, + pub payer: &'info AccountInfo, + pub compressed_token_program_authority: &'info AccountInfo, + pub spl_interface: Option>, + /// System program - required for compressible account lamport top-ups + pub system_program: &'info AccountInfo, +} + +impl<'info> TransferInterfaceCpi<'info> { + /// Create a new TransferInterfaceCpi. + #[allow(clippy::too_many_arguments)] + pub fn new( + amount: u64, + decimals: u8, + source_account: &'info AccountInfo, + destination_account: &'info AccountInfo, + authority: &'info AccountInfo, + payer: &'info AccountInfo, + compressed_token_program_authority: &'info AccountInfo, + system_program: &'info AccountInfo, + ) -> Self { + Self { + amount, + decimals, + source_account, + destination_account, + authority, + payer, + compressed_token_program_authority, + spl_interface: None, + system_program, + } + } + + /// Add SPL interface accounts (required for SPL<->light transfers). + pub fn with_spl_interface(mut self, spl_interface: SplInterfaceCpi<'info>) -> Self { + self.spl_interface = Some(spl_interface); + self + } + + /// Invoke the appropriate transfer based on account types. + pub fn invoke(self) -> Result<(), ProgramError> { + let transfer_type = determine_transfer_type( + self.source_account.owner(), + self.destination_account.owner(), + ); + + match transfer_type { + TransferType::LightToLight => TransferCpi { + source: self.source_account, + destination: self.destination_account, + amount: self.amount, + authority: self.authority, + system_program: self.system_program, + fee_payer: None, + } + .invoke(), + + TransferType::LightToSpl => { + let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + TransferToSplCpi { + source: self.source_account, + destination_spl_token_account: self.destination_account, + amount: self.amount, + authority: self.authority, + mint: spl.mint, + payer: self.payer, + spl_interface_pda: spl.spl_interface_pda, + spl_interface_pda_bump: spl.spl_interface_pda_bump, + decimals: self.decimals, + spl_token_program: spl.spl_token_program, + compressed_token_program_authority: self.compressed_token_program_authority, + } + .invoke() + } + + TransferType::SplToLight => { + let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + TransferFromSplCpi { + amount: self.amount, + spl_interface_pda_bump: spl.spl_interface_pda_bump, + decimals: self.decimals, + source_spl_token_account: self.source_account, + destination: self.destination_account, + authority: self.authority, + mint: spl.mint, + payer: self.payer, + spl_interface_pda: spl.spl_interface_pda, + spl_token_program: spl.spl_token_program, + compressed_token_program_authority: self.compressed_token_program_authority, + system_program: self.system_program, + } + .invoke() + } + + TransferType::SplToSpl => { + // For SPL-to-SPL, invoke SPL token program directly via transfer_checked + let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + + // Build SPL transfer_checked instruction data: [12, amount(8), decimals(1)] + let mut ix_data = [0u8; 10]; + ix_data[0] = SPL_TRANSFER_CHECKED_DISCRIMINATOR; + ix_data[1..9].copy_from_slice(&self.amount.to_le_bytes()); + ix_data[9] = self.decimals; + + // Account order for SPL transfer_checked: + // [0] source (writable) + // [1] mint (readonly) + // [2] destination (writable) + // [3] authority (signer) + let account_metas = [ + AccountMeta::writable(self.source_account.key()), + AccountMeta::readonly(spl.mint.key()), + AccountMeta::writable(self.destination_account.key()), + AccountMeta::readonly_signer(self.authority.key()), + ]; + + // SPL token program ID from source account owner (Pubkey = [u8; 32]) + let instruction = Instruction { + program_id: self.source_account.owner(), + accounts: &account_metas, + data: &ix_data, + }; + + let account_infos = [ + self.source_account, + spl.mint, + self.destination_account, + self.authority, + ]; + + invoke(&instruction, &account_infos) + } + } + } + + /// Invoke with signer seeds. + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + let transfer_type = determine_transfer_type( + self.source_account.owner(), + self.destination_account.owner(), + ); + + match transfer_type { + TransferType::LightToLight => TransferCpi { + source: self.source_account, + destination: self.destination_account, + amount: self.amount, + authority: self.authority, + system_program: self.system_program, + fee_payer: None, + } + .invoke_signed(signers), + + TransferType::LightToSpl => { + let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + TransferToSplCpi { + source: self.source_account, + destination_spl_token_account: self.destination_account, + amount: self.amount, + authority: self.authority, + mint: spl.mint, + payer: self.payer, + spl_interface_pda: spl.spl_interface_pda, + spl_interface_pda_bump: spl.spl_interface_pda_bump, + decimals: self.decimals, + spl_token_program: spl.spl_token_program, + compressed_token_program_authority: self.compressed_token_program_authority, + } + .invoke_signed(signers) + } + + TransferType::SplToLight => { + let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + TransferFromSplCpi { + amount: self.amount, + spl_interface_pda_bump: spl.spl_interface_pda_bump, + decimals: self.decimals, + source_spl_token_account: self.source_account, + destination: self.destination_account, + authority: self.authority, + mint: spl.mint, + payer: self.payer, + spl_interface_pda: spl.spl_interface_pda, + spl_token_program: spl.spl_token_program, + compressed_token_program_authority: self.compressed_token_program_authority, + system_program: self.system_program, + } + .invoke_signed(signers) + } + + TransferType::SplToSpl => { + // For SPL-to-SPL, invoke SPL token program directly via transfer_checked + let spl = self.spl_interface.ok_or(ProgramError::InvalidAccountData)?; + + // Build SPL transfer_checked instruction data: [12, amount(8), decimals(1)] + let mut ix_data = [0u8; 10]; + ix_data[0] = SPL_TRANSFER_CHECKED_DISCRIMINATOR; + ix_data[1..9].copy_from_slice(&self.amount.to_le_bytes()); + ix_data[9] = self.decimals; + + // Account order for SPL transfer_checked: + // [0] source (writable) + // [1] mint (readonly) + // [2] destination (writable) + // [3] authority (signer) + let account_metas = [ + AccountMeta::writable(self.source_account.key()), + AccountMeta::readonly(spl.mint.key()), + AccountMeta::writable(self.destination_account.key()), + AccountMeta::readonly_signer(self.authority.key()), + ]; + + // SPL token program ID from source account owner (Pubkey = [u8; 32]) + let instruction = Instruction { + program_id: self.source_account.owner(), + accounts: &account_metas, + data: &ix_data, + }; + + let account_infos = [ + self.source_account, + spl.mint, + self.destination_account, + self.authority, + ]; + + slice_invoke_signed(&instruction, &account_infos, signers) + } + } + } +} diff --git a/sdk-libs/token-pinocchio/src/instruction/transfer_to_spl.rs b/sdk-libs/token-pinocchio/src/instruction/transfer_to_spl.rs new file mode 100644 index 0000000000..0976853601 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/instruction/transfer_to_spl.rs @@ -0,0 +1,161 @@ +//! Transfer from CToken account to SPL token account via CPI. + +use alloc::{vec, vec::Vec}; + +use borsh::BorshSerialize; +use light_token_interface::{ + instructions::transfer2::{CompressedTokenInstructionDataTransfer2, Compression}, + LIGHT_TOKEN_PROGRAM_ID, +}; +use pinocchio::{ + account_info::AccountInfo, + cpi::{slice_invoke, slice_invoke_signed}, + instruction::{AccountMeta, Instruction, Signer}, + program_error::ProgramError, + pubkey::Pubkey, +}; + +/// Discriminator for Transfer2 instruction +const TRANSFER2_DISCRIMINATOR: u8 = 101; + +/// Transfer from CToken account to SPL token account via CPI. +/// +/// # Example +/// ```rust,ignore +/// TransferToSplCpi { +/// source: &source_ctoken, +/// destination_spl_token_account: &destination_spl, +/// amount: 100, +/// authority: &authority, +/// mint: &mint, +/// payer: &payer, +/// spl_interface_pda: &spl_interface, +/// spl_interface_pda_bump: 255, +/// decimals: 9, +/// spl_token_program: &spl_token, +/// compressed_token_program_authority: &cpi_authority, +/// } +/// .invoke()?; +/// ``` +pub struct TransferToSplCpi<'info> { + pub source: &'info AccountInfo, + pub destination_spl_token_account: &'info AccountInfo, + pub amount: u64, + pub authority: &'info AccountInfo, + pub mint: &'info AccountInfo, + pub payer: &'info AccountInfo, + pub spl_interface_pda: &'info AccountInfo, + pub spl_interface_pda_bump: u8, + pub decimals: u8, + pub spl_token_program: &'info AccountInfo, + pub compressed_token_program_authority: &'info AccountInfo, +} + +impl<'info> TransferToSplCpi<'info> { + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_signed(&[]) + } + + pub fn invoke_signed(self, signers: &[Signer]) -> Result<(), ProgramError> { + let (ix_data, account_metas, account_infos) = self.build_instruction_inner()?; + + let program_id = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let instruction = Instruction { + program_id: &program_id, + accounts: &account_metas, + data: &ix_data, + }; + + if signers.is_empty() { + slice_invoke(&instruction, &account_infos) + } else { + slice_invoke_signed(&instruction, &account_infos, signers) + } + } + + #[allow(clippy::type_complexity)] + fn build_instruction_inner( + &self, + ) -> Result<(Vec, Vec>, Vec<&AccountInfo>), ProgramError> { + // Build compressions: + // 1. Compress from ctoken account to pool + // 2. Decompress from pool to SPL token account + let compress_to_pool = Compression::compress( + self.amount, + 0, // mint index + 1, // source ctoken account index + 3, // authority index + ); + + let decompress_to_spl = Compression::decompress_spl( + self.amount, + 0, // mint index + 2, // destination SPL token account index + 4, // pool_account_index + 0, // pool_index + self.spl_interface_pda_bump, + self.decimals, + ); + + // Build instruction data + // Note: out_token_data must be empty for compressions-only (Path A) operations. + // The program determines the path based on: no_compressed_accounts = in_token_data.is_empty() && out_token_data.is_empty() + // If out_token_data is non-empty, the program expects Path B accounts (with light_system_program, registered_program_pda, etc.) + let instruction_data = CompressedTokenInstructionDataTransfer2 { + with_transaction_hash: false, + with_lamports_change_account_merkle_tree_index: false, + lamports_change_account_merkle_tree_index: 0, + lamports_change_account_owner_index: 0, + output_queue: 0, + max_top_up: 0, + cpi_context: None, + compressions: Some(vec![compress_to_pool, decompress_to_spl]), + proof: None, + in_token_data: vec![], + out_token_data: vec![], + in_lamports: None, + out_lamports: None, + in_tlv: None, + out_tlv: None, + }; + + let mut ix_data = vec![TRANSFER2_DISCRIMINATOR]; + instruction_data + .serialize(&mut ix_data) + .map_err(|_| ProgramError::BorshIoError)?; + + // Build account metas matching transfer2 format: + // [0] cpi_authority_pda (readonly) + // [1] fee_payer (signer, writable) + // [2..] packed_accounts: + // - [0] mint (readonly) + // - [1] source ctoken account (writable) + // - [2] destination SPL token account (writable) + // - [3] authority (signer, readonly) + // - [4] SPL interface PDA (writable) + // - [5] SPL Token program (readonly) + let account_metas = vec![ + AccountMeta::readonly(self.compressed_token_program_authority.key()), + AccountMeta::writable_signer(self.payer.key()), + AccountMeta::readonly(self.mint.key()), + AccountMeta::writable(self.source.key()), + AccountMeta::writable(self.destination_spl_token_account.key()), + AccountMeta::readonly_signer(self.authority.key()), + AccountMeta::writable(self.spl_interface_pda.key()), + AccountMeta::readonly(self.spl_token_program.key()), + ]; + + let account_infos = vec![ + self.compressed_token_program_authority, + self.payer, + self.mint, + self.source, + self.destination_spl_token_account, + self.authority, + self.spl_interface_pda, + self.spl_token_program, + ]; + + Ok((ix_data, account_metas, account_infos)) + } +} diff --git a/sdk-libs/token-pinocchio/src/lib.rs b/sdk-libs/token-pinocchio/src/lib.rs new file mode 100644 index 0000000000..8b772649a1 --- /dev/null +++ b/sdk-libs/token-pinocchio/src/lib.rs @@ -0,0 +1,50 @@ +//! # Light Token Pinocchio SDK +//! +//! Pinocchio-based SDK for Light Token operations via CPI. +//! +//! +//! ## CPI Operations +//! +//! | Operation | CPI Builder | +//! |-----------|-------------| +//! | Transfer | [`TransferCpi`](instruction::TransferCpi) | +//! | Transfer Checked | [`TransferCheckedCpi`](instruction::TransferCheckedCpi) | +//! | Mint To | [`MintToCpi`](instruction::MintToCpi) | +//! | Mint To Checked | [`MintToCheckedCpi`](instruction::MintToCheckedCpi) | +//! | Burn | [`BurnCpi`](instruction::BurnCpi) | +//! | Burn Checked | [`BurnCheckedCpi`](instruction::BurnCheckedCpi) | +//! | Approve | [`ApproveCpi`](instruction::ApproveCpi) | +//! | Revoke | [`RevokeCpi`](instruction::RevokeCpi) | +//! | Freeze | [`FreezeCpi`](instruction::FreezeCpi) | +//! | Thaw | [`ThawCpi`](instruction::ThawCpi) | +//! | Close Account | [`CloseAccountCpi`](instruction::CloseAccountCpi) | +//! | Create Token Account | [`CreateTokenAccountCpi`](instruction::CreateTokenAccountCpi) | +//! | Create Token ATA | [`CreateTokenAtaCpi`](instruction::CreateTokenAtaCpi) | +//! | Create Mint | [`CreateMintCpi`](instruction::CreateMintCpi) | +//! | Create Mints (Batch) | [`CreateMintsCpi`](instruction::CreateMintsCpi) | +//! | Decompress Mint | [`DecompressMintCpi`](instruction::DecompressMintCpi) | +//! +//! ## Example: Transfer via CPI +//! +//! ```rust,ignore +//! use light_token_pinocchio::instruction::TransferCpi; +//! +//! TransferCpi { +//! source: &ctx.accounts.source, +//! destination: &ctx.accounts.destination, +//! amount: 100, +//! authority: &ctx.accounts.authority, +//! system_program: &ctx.accounts.system_program, +//! max_top_up: None, +//! fee_payer: None, +//! } +//! .invoke()?; +//! ``` + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod constants; +pub mod error; +pub mod instruction; diff --git a/sdk-libs/token-sdk/Cargo.toml b/sdk-libs/token-sdk/Cargo.toml index c785e31290..076b8c4625 100644 --- a/sdk-libs/token-sdk/Cargo.toml +++ b/sdk-libs/token-sdk/Cargo.toml @@ -35,6 +35,7 @@ light-compressed-account = { workspace = true, features = ["std", "solana"] } light-compressible = { workspace = true } light-token-interface = { workspace = true } light-sdk = { workspace = true, features = ["v2", "cpi-context"] } +light-account = { workspace = true } light-batched-merkle-tree = { workspace = true } light-macros = { workspace = true } thiserror = { workspace = true } diff --git a/sdk-libs/token-sdk/README.md b/sdk-libs/token-sdk/README.md index b558a367a0..e4014d0798 100644 --- a/sdk-libs/token-sdk/README.md +++ b/sdk-libs/token-sdk/README.md @@ -2,7 +2,7 @@ # Light Token SDK -The base library to use Light Token Accounts, Light Mints, and compressed token accounts. +The base library to use Light Token Accounts, and Light Mints. ## Light Token Accounts - are on Solana devnet. @@ -15,23 +15,16 @@ The base library to use Light Token Accounts, Light Mints, and compressed token - rent is 388 lamports per rent epoch (1.5 hours). - once the account's lamports balance is insufficient, it is auto-compressed to a compressed token account. - the accounts state is cryptographically preserved on the Solana ledger. - - compressed tokens can be decompressed to a Light Token account. + - compressed tokens can be loaded to a Light Token account. - configurable lamports per write (eg transfer) keep the Light Token account perpetually funded when used. So you don't have to worry about funding rent. - users load a compressed account into a light account in-flight when using the account again. ## Light Mints - are on Solana devnet. - are Compressed accounts. -- cost 15,000 lamports to create. - support `TokenMetadata`. - have the same rent-config as light token accounts -## Compressed Token Accounts -- are on Solana mainnet. -- are compressed accounts. -- can hold Light Mint and SPL Mint tokens. -- cost 5,000 lamports to create. -- are well suited for airdrops and reward distribution. For full program examples, see the [Light Token Examples](https://github.com/Lightprotocol/examples-light-token). diff --git a/sdk-libs/token-sdk/src/anchor.rs b/sdk-libs/token-sdk/src/anchor.rs deleted file mode 100644 index 3adeb2fa47..0000000000 --- a/sdk-libs/token-sdk/src/anchor.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! Anchor integration module for Light Protocol compressed tokens. -//! -//! Provides a single import point for Anchor programs using Light Protocol. - -// Re-export Light SDK core types -pub use light_sdk::{ - account::LightAccount as LightAccountType, - address, - cpi::{v2::CpiAccounts, InvokeLightSystemProgram, LightCpiInstruction}, - derive_light_cpi_signer, derive_light_cpi_signer_pda, - error::LightSdkError, - instruction::ValidityProof, - interface::{ - CompressAs as CompressAsTrait, CompressedInitSpace, CompressionInfo, - HasCompressionInfo as HasCompressionInfoTrait, LightConfig, LightFinalize, LightPreInit, - Space, Unpack, - }, - CpiSigner, LightDiscriminator as LightDiscriminatorTrait, -}; -// Pack and PackedAccounts only available off-chain (client-side) -#[cfg(not(target_os = "solana"))] -pub use light_sdk::{instruction::PackedAccounts, interface::Pack}; -// Re-export Light SDK macros -pub use light_sdk_macros::{ - // Proc macros - derive_light_rent_sponsor, - derive_light_rent_sponsor_pda, - // Attribute macros - light_program, - // Derive macros - AnchorDiscriminator, - CompressAs, - Compressible, - HasCompressionInfo, - LightAccount, - LightAccounts, - LightDiscriminator, - LightHasher, - LightHasherSha, -}; - -// Re-export token SDK types -pub use crate::{instruction::*, CompressedProof, ValidityProof as ValidityProofAlias}; diff --git a/sdk-libs/token-sdk/src/compressible/compress_runtime.rs b/sdk-libs/token-sdk/src/compressible/compress_runtime.rs deleted file mode 100644 index 11f29e8c41..0000000000 --- a/sdk-libs/token-sdk/src/compressible/compress_runtime.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Runtime helpers for compressing PDAs to Light Protocol. - -use light_compressed_account::instruction_data::{ - data::NewAddressParamsAssignedPacked, with_account_info::CompressedAccountInfo, -}; -use light_sdk::{ - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - instruction::ValidityProof, -}; -use light_sdk_types::CpiSigner; -use solana_program_error::ProgramError; - -use crate::error::LightTokenError; -// TODO: rename file -/// Write PDAs to CPI context for chaining with mint operations. -/// -/// Use this when PDAs need to be written to CPI context first, which will be -/// consumed by subsequent mint operations (e.g., CreateMintsCpi). -/// -/// # Arguments -/// * `cpi_signer` - CPI signer for the invoking program -/// * `proof` - Validity proof for the compression operation -/// * `new_addresses` - New address parameters for each PDA -/// * `compressed_infos` - Compressed account info for each PDA -/// * `cpi_accounts` - CPI accounts with CPI context enabled -pub fn invoke_write_pdas_to_cpi_context<'info>( - cpi_signer: CpiSigner, - proof: ValidityProof, - new_addresses: &[NewAddressParamsAssignedPacked], - compressed_infos: &[CompressedAccountInfo], - cpi_accounts: &CpiAccounts<'_, 'info>, -) -> Result<(), ProgramError> { - let cpi_context_account = cpi_accounts - .cpi_context() - .map_err(|_| LightTokenError::MissingCpiContext)?; - let cpi_context_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts - .authority() - .map_err(|_| LightTokenError::MissingCpiAuthority)?, - cpi_context: cpi_context_account, - cpi_signer, - }; - - LightSystemProgramCpi::new_cpi(cpi_signer, proof) - .with_new_addresses(new_addresses) - .with_account_infos(compressed_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(cpi_context_accounts)?; - - Ok(()) -} diff --git a/sdk-libs/token-sdk/src/compressible/mint_runtime.rs b/sdk-libs/token-sdk/src/compressible/mint_runtime.rs deleted file mode 100644 index 4899e1b081..0000000000 --- a/sdk-libs/token-sdk/src/compressible/mint_runtime.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! Runtime helpers for compressed mint creation. -//! -//! These functions consolidate the CPI setup logic used by `#[derive(LightAccounts)]` -//! macro for mint creation, reducing macro complexity and SDK coupling. - -use light_sdk::cpi::v2::CpiAccounts; -use solana_account_info::AccountInfo; -use solana_program_error::ProgramError; - -use crate::{ - error::LightTokenError, - instruction::{CreateMintsCpi, CreateMintsParams, SystemAccountInfos}, -}; - -/// Infrastructure accounts needed for mint creation CPI. -/// -/// These accounts are passed from the user's Accounts struct. -pub struct CreateMintsInfraAccounts<'info> { - /// Fee payer for the transaction. - pub fee_payer: AccountInfo<'info>, - /// CompressibleConfig account for the light-token program. - pub compressible_config: AccountInfo<'info>, - /// Rent sponsor PDA. - pub rent_sponsor: AccountInfo<'info>, - /// CPI authority PDA for signing. - pub cpi_authority: AccountInfo<'info>, -} - -/// Invoke CreateMintsCpi to create and decompress compressed mints. -/// -/// This function handles: -/// - Extracting tree accounts from CpiAccounts -/// - Building the SystemAccountInfos -/// - Constructing and invoking CreateMintsCpi -/// -/// # Arguments -/// * `mint_seed_accounts` - AccountInfos for mint signers (one per mint) -/// * `mint_accounts` - AccountInfos for mint PDAs (one per mint) -/// * `params` - CreateMintsParams with mint params and configuration -/// * `infra` - Infrastructure accounts from the Accounts struct -/// * `cpi_accounts` - CpiAccounts for accessing system accounts -#[inline(never)] -pub fn invoke_create_mints<'a, 'info>( - mint_seed_accounts: &'a [AccountInfo<'info>], - mint_accounts: &'a [AccountInfo<'info>], - params: CreateMintsParams<'a>, - infra: CreateMintsInfraAccounts<'info>, - cpi_accounts: &CpiAccounts<'_, 'info>, -) -> Result<(), ProgramError> { - // Extract tree accounts from CpiAccounts - let output_queue = cpi_accounts - .get_tree_account_info(params.output_queue_index as usize) - .map_err(|_| LightTokenError::MissingOutputQueue)? - .clone(); - let state_merkle_tree = cpi_accounts - .get_tree_account_info(params.state_tree_index as usize) - .map_err(|_| LightTokenError::MissingStateMerkleTree)? - .clone(); - let address_tree = cpi_accounts - .get_tree_account_info(params.address_tree_index as usize) - .map_err(|_| LightTokenError::MissingAddressMerkleTree)? - .clone(); - - // Build system accounts from CpiAccounts - let system_accounts = SystemAccountInfos { - light_system_program: cpi_accounts - .light_system_program() - .map_err(|_| LightTokenError::MissingLightSystemProgram)? - .clone(), - cpi_authority_pda: infra.cpi_authority, - registered_program_pda: cpi_accounts - .registered_program_pda() - .map_err(|_| LightTokenError::MissingRegisteredProgramPda)? - .clone(), - account_compression_authority: cpi_accounts - .account_compression_authority() - .map_err(|_| LightTokenError::MissingAccountCompressionAuthority)? - .clone(), - account_compression_program: cpi_accounts - .account_compression_program() - .map_err(|_| LightTokenError::MissingAccountCompressionProgram)? - .clone(), - system_program: cpi_accounts - .system_program() - .map_err(|_| LightTokenError::MissingSystemProgram)? - .clone(), - }; - - // Build and invoke CreateMintsCpi - CreateMintsCpi { - mint_seed_accounts, - payer: infra.fee_payer, - address_tree, - output_queue, - state_merkle_tree, - compressible_config: infra.compressible_config, - mints: mint_accounts, - rent_sponsor: infra.rent_sponsor, - system_accounts, - cpi_context_account: cpi_accounts - .cpi_context() - .map_err(|_| LightTokenError::MissingCpiContext)? - .clone(), - params, - } - .invoke() -} diff --git a/sdk-libs/token-sdk/src/compressible/mod.rs b/sdk-libs/token-sdk/src/compressible/mod.rs deleted file mode 100644 index 31cdee3b36..0000000000 --- a/sdk-libs/token-sdk/src/compressible/mod.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! Compressible token utilities for runtime compression and decompression. - -mod compress_runtime; -mod mint_runtime; - -pub use compress_runtime::*; -pub use mint_runtime::*; -use solana_account_info::AccountInfo; - -#[derive(Debug, Clone)] -pub struct AccountInfoToCompress<'info> { - pub account_info: AccountInfo<'info>, - pub signer_seeds: Vec>, -} diff --git a/sdk-libs/token-sdk/src/constants.rs b/sdk-libs/token-sdk/src/constants.rs index 974c83acf8..ef7ac5fb44 100644 --- a/sdk-libs/token-sdk/src/constants.rs +++ b/sdk-libs/token-sdk/src/constants.rs @@ -8,16 +8,16 @@ pub use light_compressed_token_sdk::constants::*; use light_compressible::config::CompressibleConfig; use solana_pubkey::{pubkey, Pubkey}; -/// CPI Authority PDA for the Compressed Token Program +/// CPI Authority PDA for the Light Token Program pub const LIGHT_TOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); -/// Returns the program ID for the Compressed Token Program +/// Returns the program ID for the Light Token Program pub fn id() -> Pubkey { LIGHT_TOKEN_PROGRAM_ID } -/// Return the cpi authority pda of the Compressed Token Program. +/// Return the CPI authority PDA of the Light Token Program. pub fn cpi_authority() -> Pubkey { LIGHT_TOKEN_CPI_AUTHORITY } diff --git a/sdk-libs/token-sdk/src/instruction/create_mint.rs b/sdk-libs/token-sdk/src/instruction/create_mint.rs index 890cecd706..72b49ee513 100644 --- a/sdk-libs/token-sdk/src/instruction/create_mint.rs +++ b/sdk-libs/token-sdk/src/instruction/create_mint.rs @@ -21,7 +21,7 @@ use crate::{ }; /// Parameters for creating a mint. /// -/// Creates both a compressed mint AND a decompressed Mint Solana account +/// Creates both a Light Mint AND a decompressed Mint Solana account /// in a single instruction. #[derive(Debug, Clone)] pub struct CreateMintParams { @@ -42,7 +42,7 @@ pub struct CreateMintParams { pub write_top_up: u32, } -/// Create a mint instruction that creates both a compressed mint AND a Mint Solana account. +/// Create a mint instruction that creates both a Light Mint AND a Mint Solana account. /// /// # Example /// ```rust,no_run @@ -366,7 +366,7 @@ impl<'info> TryFrom<&CreateMintCpi<'info>> for CreateMint { // Helper Functions // ============================================================================ -/// Derives the compressed mint address from the mint seed and address tree +/// Derives the Light Mint address from the mint seed and address tree pub fn derive_mint_compressed_address( mint_seed: &Pubkey, address_tree_pubkey: &Pubkey, @@ -378,7 +378,7 @@ pub fn derive_mint_compressed_address( ) } -/// Derives the compressed mint address from an SPL mint address +/// Derives the Light Mint address from an SPL mint address pub fn derive_mint_from_spl_mint(mint: &Pubkey, address_tree_pubkey: &Pubkey) -> [u8; 32] { light_compressed_account::address::derive_address( &mint.to_bytes(), @@ -387,7 +387,7 @@ pub fn derive_mint_from_spl_mint(mint: &Pubkey, address_tree_pubkey: &Pubkey) -> ) } -/// Finds the compressed mint address from a mint seed. +/// Finds the Light Mint address from a mint seed. pub fn find_mint_address(mint_seed: &Pubkey) -> (Pubkey, u8) { Pubkey::find_program_address( &[COMPRESSED_MINT_SEED, mint_seed.as_ref()], diff --git a/sdk-libs/token-sdk/src/instruction/create_mints.rs b/sdk-libs/token-sdk/src/instruction/create_mints.rs index a69ec27204..d1a9629cdf 100644 --- a/sdk-libs/token-sdk/src/instruction/create_mints.rs +++ b/sdk-libs/token-sdk/src/instruction/create_mints.rs @@ -1,6 +1,6 @@ -//! Create multiple compressed mints and decompress all to Solana Mint accounts. +//! Create multiple Light Mints and decompress all to Solana Mint accounts. //! -//! This module provides functionality for batch creating compressed mints with +//! This module provides functionality for batch creating Light Mints with //! optimal CPI batching. When creating multiple mints, it uses the CPI context //! pattern to minimize transaction overhead. //! @@ -41,16 +41,15 @@ pub const DEFAULT_WRITE_TOP_UP: u32 = 766; /// Parameters for a single mint within a batch creation. /// /// Does not include proof since proof is shared across all mints in the batch. +/// `mint` and `compression_address` are derived internally from `mint_seed_pubkey`. #[derive(Debug, Clone)] pub struct SingleMintParams<'a> { pub decimals: u8, - pub address_merkle_tree_root_index: u16, pub mint_authority: Pubkey, - pub compression_address: [u8; 32], - pub mint: Pubkey, - pub bump: u8, + /// Optional mint bump. If `None`, derived from `find_mint_address(mint_seed_pubkey)`. + pub mint_bump: Option, pub freeze_authority: Option, - /// Mint seed pubkey (signer) for this mint + /// Mint seed pubkey (signer) for this mint. Used to derive `mint` PDA and `compression_address`. pub mint_seed_pubkey: Pubkey, /// Optional authority seeds for PDA signing pub authority_seeds: Option<&'a [&'a [u8]]>, @@ -60,9 +59,9 @@ pub struct SingleMintParams<'a> { pub token_metadata: Option<&'a TokenMetadataInstructionData>, } -/// Parameters for creating one or more compressed mints with decompression. +/// Parameters for creating one or more Light Mints with decompression. /// -/// Creates N compressed mints and decompresses all to Solana Mint accounts. +/// Creates N Light Mints and decompresses all to Solana Mint accounts. /// Uses CPI context pattern when N > 1 for efficiency. #[derive(Debug, Clone)] pub struct CreateMintsParams<'a> { @@ -70,6 +69,8 @@ pub struct CreateMintsParams<'a> { pub mints: &'a [SingleMintParams<'a>], /// Single proof covering all new addresses pub proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof, + /// Root index for the address merkle tree (shared by all mints in batch). + pub address_merkle_tree_root_index: u16, /// Rent payment in epochs for the Mint account (must be 0 or >= 2). /// Default: 16 (~24 hours) pub rent_payment: u8, @@ -98,10 +99,12 @@ impl<'a> CreateMintsParams<'a> { pub fn new( mints: &'a [SingleMintParams<'a>], proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof, + address_merkle_tree_root_index: u16, ) -> Self { Self { mints, proof, + address_merkle_tree_root_index, rent_payment: DEFAULT_RENT_PAYMENT, write_top_up: DEFAULT_WRITE_TOP_UP, cpi_context_offset: 0, @@ -239,8 +242,10 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { #[inline(never)] fn invoke_single_mint(self) -> Result<(), ProgramError> { let mint_params = &self.params.mints[0]; + let (mint, bump) = get_mint_and_bump(mint_params); - let mint_data = build_mint_instruction_data(mint_params, self.mint_seed_accounts[0].key); + let mint_data = + build_mint_instruction_data(mint_params, self.mint_seed_accounts[0].key, mint, bump); let decompress_action = DecompressMintAction { rent_payment: self.params.rent_payment, @@ -248,7 +253,7 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { }; let instruction_data = MintActionCompressedInstructionData::new_mint( - mint_params.address_merkle_tree_root_index, + self.params.address_merkle_tree_root_index, self.params.proof, mint_data, ) @@ -306,6 +311,7 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { fn invoke_cpi_write(&self, index: usize) -> Result<(), ProgramError> { let mint_params = &self.params.mints[index]; let offset = self.params.cpi_context_offset; + let (mint, bump) = get_mint_and_bump(mint_params); // When sharing CPI context with PDAs: // - first_set_context: only true for index 0 AND offset 0 (first write to context) @@ -323,11 +329,15 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { address_tree_pubkey: self.address_tree.key.to_bytes(), }; - let mint_data = - build_mint_instruction_data(mint_params, self.mint_seed_accounts[index].key); + let mint_data = build_mint_instruction_data( + mint_params, + self.mint_seed_accounts[index].key, + mint, + bump, + ); let instruction_data = MintActionCompressedInstructionData::new_mint_write_to_cpi_context( - mint_params.address_merkle_tree_root_index, + self.params.address_merkle_tree_root_index, mint_data, cpi_context, ); @@ -389,15 +399,20 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { ) -> Result<(), ProgramError> { let mint_params = &self.params.mints[last_idx]; let offset = self.params.cpi_context_offset; + let (mint, bump) = get_mint_and_bump(mint_params); - let mint_data = - build_mint_instruction_data(mint_params, self.mint_seed_accounts[last_idx].key); + let mint_data = build_mint_instruction_data( + mint_params, + self.mint_seed_accounts[last_idx].key, + mint, + bump, + ); // Create struct directly to reduce stack usage (avoid builder pattern intermediates) let instruction_data = MintActionCompressedInstructionData { leaf_index: 0, prove_by_index: false, - root_index: mint_params.address_merkle_tree_root_index, + root_index: self.params.address_merkle_tree_root_index, max_top_up: 0, create_mint: Some(CreateMint::default()), actions: vec![Action::DecompressMint(*decompress_action)], @@ -444,9 +459,14 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { decompress_action: &DecompressMintAction, ) -> Result<(), ProgramError> { let mint_params = &self.params.mints[index]; + let (mint, bump) = get_mint_and_bump(mint_params); - let mint_data = - build_mint_instruction_data(mint_params, self.mint_seed_accounts[index].key); + let mint_data = build_mint_instruction_data( + mint_params, + self.mint_seed_accounts[index].key, + mint, + bump, + ); let instruction_data = MintActionCompressedInstructionData { leaf_index: base_leaf_index + index as u32, @@ -544,11 +564,23 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { } } +/// Get mint PDA and bump, deriving mint always and bump if None. +#[inline(never)] +fn get_mint_and_bump(params: &SingleMintParams) -> (Pubkey, u8) { + let (mint, derived_bump) = super::find_mint_address(¶ms.mint_seed_pubkey); + let bump = params.mint_bump.unwrap_or(derived_bump); + (mint, bump) +} + /// Build MintInstructionData for a single mint. +/// +/// `mint` and `bump` are derived externally from `mint_seed_pubkey` using `get_mint_and_bump`. #[inline(never)] fn build_mint_instruction_data( mint_params: &SingleMintParams<'_>, mint_signer: &Pubkey, + mint: Pubkey, + bump: u8, ) -> MintInstructionData { // Convert token_metadata to extensions if present let extensions = mint_params @@ -561,10 +593,10 @@ fn build_mint_instruction_data( decimals: mint_params.decimals, metadata: MintMetadata { version: 3, - mint: mint_params.mint.to_bytes().into(), + mint: mint.to_bytes().into(), mint_decompressed: false, mint_signer: mint_signer.to_bytes(), - bump: mint_params.bump, + bump, }, mint_authority: Some(mint_params.mint_authority.to_bytes().into()), freeze_authority: mint_params.freeze_authority.map(|a| a.to_bytes().into()), diff --git a/sdk-libs/token-sdk/src/instruction/decompress.rs b/sdk-libs/token-sdk/src/instruction/decompress.rs index 7c8b6bff02..163ff6a05f 100644 --- a/sdk-libs/token-sdk/src/instruction/decompress.rs +++ b/sdk-libs/token-sdk/src/instruction/decompress.rs @@ -1,3 +1,4 @@ +use light_account::PackedAccounts; use light_compressed_account::instruction_data::compressed_proof::ValidityProof; use light_compressed_token_sdk::compressed_token::{ decompress_full::pack_for_decompress_full_with_ata, @@ -6,7 +7,7 @@ use light_compressed_token_sdk::compressed_token::{ }, CTokenAccount2, }; -use light_sdk::instruction::{PackedAccounts, PackedStateTreeInfo}; +use light_sdk::instruction::PackedStateTreeInfo; use light_token_interface::{ instructions::extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, state::{AccountState, ExtensionStruct, TokenData, TokenDataVersion}, diff --git a/sdk-libs/token-sdk/src/instruction/decompress_mint.rs b/sdk-libs/token-sdk/src/instruction/decompress_mint.rs index 4e1e679db0..da399c800c 100644 --- a/sdk-libs/token-sdk/src/instruction/decompress_mint.rs +++ b/sdk-libs/token-sdk/src/instruction/decompress_mint.rs @@ -16,7 +16,7 @@ use crate::{ instruction::SystemAccountInfos, }; -/// Decompress a compressed mint to a Mint Solana account. +/// Decompress a Light Mint to a Mint Solana account. /// /// Creates an on-chain Mint PDA that becomes the source of truth. /// The Mint is always compressible. @@ -41,15 +41,15 @@ pub struct DecompressMint { pub payer: Pubkey, /// Mint authority (must sign) pub authority: Pubkey, - /// State tree for the compressed mint + /// State tree for the Light Mint pub state_tree: Pubkey, - /// Input queue for reading compressed mint + /// Input queue for reading Light Mint pub input_queue: Pubkey, - /// Output queue for updated compressed mint + /// Output queue for updated Light Mint pub output_queue: Pubkey, - /// Compressed mint with context (from indexer) + /// Light Mint with context (from indexer) pub compressed_mint_with_context: MintWithContext, - /// Validity proof for the compressed mint + /// Validity proof for the Light Mint pub proof: ValidityProof, /// Rent payment in epochs (must be >= 2) pub rent_payment: u8, @@ -59,7 +59,7 @@ pub struct DecompressMint { impl DecompressMint { pub fn instruction(self) -> Result { - // Get Mint PDA from compressed mint metadata + // Get Mint PDA from Light Mint metadata let mint_data = self .compressed_mint_with_context .mint @@ -108,7 +108,7 @@ impl DecompressMint { // CPI Struct: DecompressMintCpi // ============================================================================ -/// Decompress a compressed mint to a Mint Solana account via CPI. +/// Decompress a Light Mint to a Mint Solana account via CPI. /// /// Creates an on-chain Mint PDA that becomes the source of truth. /// The Mint is always compressible. @@ -143,17 +143,17 @@ pub struct DecompressMintCpi<'info> { pub compressible_config: AccountInfo<'info>, /// Rent sponsor PDA account pub rent_sponsor: AccountInfo<'info>, - /// State tree for the compressed mint + /// State tree for the Light Mint pub state_tree: AccountInfo<'info>, - /// Input queue for reading compressed mint + /// Input queue for reading Light Mint pub input_queue: AccountInfo<'info>, - /// Output queue for updated compressed mint + /// Output queue for updated Light Mint pub output_queue: AccountInfo<'info>, /// System accounts for Light Protocol pub system_accounts: SystemAccountInfos<'info>, - /// Compressed mint with context (from indexer) + /// Light Mint with context (from indexer) pub compressed_mint_with_context: MintWithContext, - /// Validity proof for the compressed mint + /// Validity proof for the Light Mint pub proof: ValidityProof, /// Rent payment in epochs (must be >= 2) pub rent_payment: u8, @@ -233,7 +233,7 @@ impl<'info> TryFrom<&DecompressMintCpi<'info>> for DecompressMint { } } -/// Decompress a compressed mint with CPI context support. +/// Decompress a Light 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. @@ -245,15 +245,15 @@ pub struct DecompressCMintWithCpiContext { pub payer: Pubkey, /// Mint authority (must sign) pub authority: Pubkey, - /// State tree for the compressed mint + /// State tree for the Light Mint pub state_tree: Pubkey, - /// Input queue for reading compressed mint + /// Input queue for reading Light Mint pub input_queue: Pubkey, - /// Output queue for updated compressed mint + /// Output queue for updated Light Mint pub output_queue: Pubkey, - /// Compressed mint with context (from indexer) + /// Light Mint with context (from indexer) pub compressed_mint_with_context: MintWithContext, - /// Validity proof for the compressed mint + /// Validity proof for the Light Mint pub proof: ValidityProof, /// Rent payment in epochs (must be >= 2) pub rent_payment: u8, @@ -329,11 +329,11 @@ pub struct DecompressCMintCpiWithContext<'info> { pub compressible_config: AccountInfo<'info>, /// Rent sponsor PDA account pub rent_sponsor: AccountInfo<'info>, - /// State tree for the compressed mint + /// State tree for the Light Mint pub state_tree: AccountInfo<'info>, - /// Input queue for reading compressed mint + /// Input queue for reading Light Mint pub input_queue: AccountInfo<'info>, - /// Output queue for updated compressed mint + /// Output queue for updated Light Mint pub output_queue: AccountInfo<'info>, /// CPI context account pub cpi_context_account: AccountInfo<'info>, @@ -342,9 +342,9 @@ pub struct DecompressCMintCpiWithContext<'info> { /// Light token program's CPI authority (GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy) /// This is separate from system_accounts.cpi_authority_pda which is the calling program's authority pub light_token_cpi_authority: AccountInfo<'info>, - /// Compressed mint with context (from indexer) + /// Light Mint with context (from indexer) pub compressed_mint_with_context: MintWithContext, - /// Validity proof for the compressed mint + /// Validity proof for the Light Mint pub proof: ValidityProof, /// Rent payment in epochs (must be >= 2) pub rent_payment: u8, diff --git a/sdk-libs/token-sdk/src/instruction/mod.rs b/sdk-libs/token-sdk/src/instruction/mod.rs index c0a30d033b..adc6df19e2 100644 --- a/sdk-libs/token-sdk/src/instruction/mod.rs +++ b/sdk-libs/token-sdk/src/instruction/mod.rs @@ -156,7 +156,7 @@ pub use transfer_to_spl::{TransferToSpl, TransferToSplCpi}; /// System accounts required for CPI operations to Light Protocol. /// -/// Pass these accounts when invoking compressed token operations from your program. +/// Pass these accounts when invoking Light Token operations from your program. /// /// # Fields /// diff --git a/sdk-libs/token-sdk/src/instruction/transfer_interface.rs b/sdk-libs/token-sdk/src/instruction/transfer_interface.rs index dabe1029a9..83720915e9 100644 --- a/sdk-libs/token-sdk/src/instruction/transfer_interface.rs +++ b/sdk-libs/token-sdk/src/instruction/transfer_interface.rs @@ -263,7 +263,7 @@ impl<'info> TransferInterfaceCpi<'info> { /// * `destination_account` - Destination token account (can be light or SPL) /// * `authority` - Authority for the transfer (must be signer) /// * `payer` - Payer for the transaction - /// * `compressed_token_program_authority` - Compressed token program authority + /// * `compressed_token_program_authority` - Light Token program authority /// * `system_program` - System program (required for compressible account lamport top-ups) #[allow(clippy::too_many_arguments)] pub fn new( diff --git a/sdk-libs/token-sdk/src/lib.rs b/sdk-libs/token-sdk/src/lib.rs index 2feafee8f1..642d443ebf 100644 --- a/sdk-libs/token-sdk/src/lib.rs +++ b/sdk-libs/token-sdk/src/lib.rs @@ -1,6 +1,6 @@ //! # Light Token SDK //! -//! The base library to use Light Token Accounts, Light Mints, and compressed token accounts. +//! The base library to use Light Token Accounts, and Light Mints. //! //! ## Light Token Accounts //! - are on Solana devnet. @@ -13,23 +13,16 @@ //! - rent is 388 lamports per rent epoch (1.5 hours). //! - once the account's lamports balance is insufficient, it is auto-compressed to a compressed token account. //! - the accounts state is cryptographically preserved on the Solana ledger. -//! - compressed tokens can be decompressed to a Light Token account. +//! - compressed tokens can be loaded to a Light Token account. //! - configurable lamports per write (eg transfer) keep the Light Token account perpetually funded when used. So you don't have to worry about funding rent. //! - users load a compressed account into a light account in-flight when using the account again. //! //! ## Light Mints //! - are on Solana devnet. //! - are Compressed accounts. -//! - cost 15,000 lamports to create. //! - support `TokenMetadata`. //! - have the same rent-config as light token accounts //! -//! ## Compressed Token Accounts -//! - are on Solana mainnet. -//! - are compressed accounts. -//! - can hold Light Mint and SPL Mint tokens. -//! - cost 5,000 lamports to create. -//! - are well suited for airdrops and reward distribution. //! //! For full program examples, see the [Light Token Examples](https://github.com/Lightprotocol/examples-light-token). //! @@ -70,13 +63,9 @@ //! # Disclaimer //! This library is not audited and in a beta state. Use at your own risk and expect breaking changes. -#[cfg(feature = "anchor")] -pub mod anchor; -pub mod compressible; pub mod constants; pub mod error; pub mod instruction; -// pub mod pack; pub mod spl_interface; pub mod utils; diff --git a/sdk-libs/token-sdk/src/pack.rs b/sdk-libs/token-sdk/src/pack.rs deleted file mode 100644 index 025d0353af..0000000000 --- a/sdk-libs/token-sdk/src/pack.rs +++ /dev/null @@ -1,218 +0,0 @@ -//! Pack implementation for TokenData types for c-tokens. -use light_compressed_account::compressed_account::CompressedAccountWithMerkleContext; -use light_sdk::{instruction::PackedAccounts, light_hasher::HasherError}; -pub use light_token_interface::state::TokenData; -use light_token_interface::state::TokenDataVersion; -use solana_account_info::AccountInfo; -use solana_program_error::ProgramError; -// TODO: remove -use crate::{AnchorDeserialize, AnchorSerialize}; - -// 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::interface. -pub trait Pack { - type Packed; - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Result; -} -pub trait Unpack { - type Unpacked; - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result; -} - -/// Solana-compatible token types using `solana_pubkey::Pubkey` -pub mod compat { - // Re-export TokenData and AccountState from compressed-token-sdk for type compatibility - pub use light_compressed_token_sdk::compat::{ - AccountState, InputTokenDataCompressible, TokenData, - }; - - use super::*; - - /// TokenData with merkle context for verification - #[derive(Debug, Clone, PartialEq)] - pub struct TokenDataWithMerkleContext { - pub token_data: TokenData, - pub compressed_account: CompressedAccountWithMerkleContext, - } - - impl TokenDataWithMerkleContext { - /// Only works for sha flat hash - pub fn hash(&self) -> Result<[u8; 32], HasherError> { - if let Some(data) = self.compressed_account.compressed_account.data.as_ref() { - match data.discriminator { - [0, 0, 0, 0, 0, 0, 0, 4] => self.token_data.hash_sha_flat(), - _ => Err(HasherError::EmptyInput), - } - } else { - Err(HasherError::EmptyInput) - } - } - } - - impl Pack for TokenData { - type Packed = InputTokenDataCompressible; - - fn pack( - &self, - remaining_accounts: &mut PackedAccounts, - ) -> Result { - Ok(InputTokenDataCompressible { - owner: remaining_accounts.insert_or_get(self.owner), - mint: remaining_accounts.insert_or_get_read_only(self.mint), - amount: self.amount, - has_delegate: self.delegate.is_some(), - delegate: if let Some(delegate) = self.delegate { - remaining_accounts.insert_or_get(delegate) - } 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()) - } - } - - impl Unpack for InputTokenDataCompressible { - type Unpacked = TokenData; - - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(TokenData { - owner: *remaining_accounts - .get(self.owner as usize) - .ok_or(ProgramError::InvalidAccountData)? - .key, - amount: self.amount, - delegate: if self.has_delegate { - Some( - *remaining_accounts - .get(self.delegate as usize) - .ok_or(ProgramError::InvalidAccountData)? - .key, - ) - } else { - None - }, - mint: *remaining_accounts - .get(self.mint as usize) - .ok_or(ProgramError::InvalidAccountData)? - .key, - state: AccountState::Initialized, - tlv: None, - }) - } - } - - /// Wrapper for token data with variant information - #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] - pub struct TokenDataWithVariant { - pub variant: V, - pub token_data: TokenData, - } - - #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] - pub struct PackedTokenDataWithVariant { - pub variant: V, - pub token_data: InputTokenDataCompressible, - } - - #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] - pub struct CTokenDataWithVariant { - pub variant: V, - pub token_data: TokenData, - } - - impl Pack for CTokenDataWithVariant - where - V: Pack, - V::Packed: AnchorSerialize + Clone + std::fmt::Debug, - { - type Packed = PackedTokenDataWithVariant; - - fn pack( - &self, - remaining_accounts: &mut PackedAccounts, - ) -> Result { - Ok(PackedTokenDataWithVariant { - variant: self.variant.pack(remaining_accounts)?, - token_data: self.token_data.pack(remaining_accounts)?, - }) - } - } - - impl Unpack for CTokenDataWithVariant - where - V: Clone, - { - type Unpacked = TokenDataWithVariant; - - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - // Note: This impl assumes V is already unpacked (has Pubkeys). - // For packed variants, use PackedTokenDataWithVariant::unpack instead. - Ok(TokenDataWithVariant { - variant: self.variant.clone(), - token_data: self.token_data.unpack(remaining_accounts)?, - }) - } - } - - impl Pack for TokenDataWithVariant - where - V: Pack, - V::Packed: AnchorSerialize + Clone + std::fmt::Debug, - { - type Packed = PackedTokenDataWithVariant; - - fn pack( - &self, - remaining_accounts: &mut PackedAccounts, - ) -> Result { - Ok(PackedTokenDataWithVariant { - variant: self.variant.pack(remaining_accounts)?, - token_data: self.token_data.pack(remaining_accounts)?, - }) - } - } - - impl Unpack for PackedTokenDataWithVariant - where - V: Unpack, - { - type Unpacked = TokenDataWithVariant; - - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(TokenDataWithVariant { - variant: self.variant.unpack(remaining_accounts)?, - token_data: self.token_data.unpack(remaining_accounts)?, - }) - } - } - - // TODO: remove aliases in separate PR - pub type CompressibleTokenDataWithVariant = CTokenDataWithVariant; - pub type PackedCompressibleTokenDataWithVariant = PackedTokenDataWithVariant; - pub type CTokenData = CTokenDataWithVariant; - pub type PackedCTokenData = PackedTokenDataWithVariant; -} diff --git a/sdk-libs/token-sdk/tests/pack_test.rs b/sdk-libs/token-sdk/tests/pack_test.rs index b2e98e58a5..2f58f39063 100644 --- a/sdk-libs/token-sdk/tests/pack_test.rs +++ b/sdk-libs/token-sdk/tests/pack_test.rs @@ -1,6 +1,6 @@ #![cfg(feature = "compressible")] -use light_sdk::instruction::PackedAccounts; +use light_account::PackedAccounts; use light_token::{ compat::{PackedCompressibleTokenDataWithVariant, TokenData, TokenDataWithVariant}, pack::Pack, diff --git a/sdk-tests/manual-test/Cargo.toml b/sdk-tests/anchor-manual-test/Cargo.toml similarity index 78% rename from sdk-tests/manual-test/Cargo.toml rename to sdk-tests/anchor-manual-test/Cargo.toml index ec99498b80..8e9491fc80 100644 --- a/sdk-tests/manual-test/Cargo.toml +++ b/sdk-tests/anchor-manual-test/Cargo.toml @@ -1,37 +1,33 @@ [package] -name = "manual-test" +name = "anchor-manual-test" version = "0.1.0" description = "Manual LightAccount implementation test without macros" edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "manual_test" +name = "anchor_manual_test" [features] no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] -custom-heap = ["light-heap", "light-sdk/custom-heap"] default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +idl-build = ["anchor-lang/idl-build"] test-sbf = [] [dependencies] -light-heap = { workspace = true, optional = true } -light-sdk = { workspace = true, features = ["anchor", "v2", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +light-account = { workspace = true, features = ["token", "sha256"] } light-macros = { workspace = true, features = ["solana"] } -light-sdk-macros = { workspace = true } borsh = { workspace = true } bytemuck = { workspace = true } light-compressed-account = { workspace = true, features = ["solana"] } anchor-lang = { workspace = true } light-compressible = { workspace = true, features = ["anchor"] } light-hasher = { workspace = true, features = ["solana"] } -light-token = { workspace = true, features = ["anchor"] } light-token-types = { workspace = true, features = ["anchor"] } +light-token-interface = { workspace = true } solana-program = { workspace = true } solana-pubkey = { workspace = true } solana-msg = { workspace = true } @@ -42,9 +38,8 @@ solana-account-info = { workspace = true } light-program-test = { workspace = true, features = ["devenv"] } light-client = { workspace = true, features = ["v2", "anchor"] } light-test-utils = { workspace = true } -light-token = { workspace = true } +light-token = { workspace = true, features = ["anchor"] } light-token-client = { workspace = true } -light-token-interface = { workspace = true } tokio = { workspace = true } solana-sdk = { workspace = true } solana-instruction = { workspace = true } diff --git a/sdk-tests/manual-test/src/account_loader/accounts.rs b/sdk-tests/anchor-manual-test/src/account_loader/accounts.rs similarity index 96% rename from sdk-tests/manual-test/src/account_loader/accounts.rs rename to sdk-tests/anchor-manual-test/src/account_loader/accounts.rs index cbf1ec7edc..1d8c3662ec 100644 --- a/sdk-tests/manual-test/src/account_loader/accounts.rs +++ b/sdk-tests/anchor-manual-test/src/account_loader/accounts.rs @@ -1,7 +1,7 @@ //! Accounts module for zero-copy account instruction. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; +use light_account::CreateAccountsProof; use super::state::ZeroCopyRecord; diff --git a/sdk-tests/manual-test/src/account_loader/derived_accounts.rs b/sdk-tests/anchor-manual-test/src/account_loader/derived_accounts.rs similarity index 50% rename from sdk-tests/manual-test/src/account_loader/derived_accounts.rs rename to sdk-tests/anchor-manual-test/src/account_loader/derived_accounts.rs index 02c8f2c903..f3bdc41516 100644 --- a/sdk-tests/manual-test/src/account_loader/derived_accounts.rs +++ b/sdk-tests/anchor-manual-test/src/account_loader/derived_accounts.rs @@ -4,21 +4,15 @@ //! adapted for the AccountLoader (zero-copy) access pattern. use anchor_lang::prelude::*; +use light_account::{ + light_account_checks::{self, packed_accounts::ProgramPackedAccounts}, + prepare_compressed_account_on_init, CpiAccounts, CpiAccountsConfig, CpiContextWriteAccounts, + InvokeLightSystemProgram, LightAccount, LightAccountVariantTrait, LightFinalize, LightPreInit, + LightSdkTypesError, PackedAddressTreeInfoExt, PackedLightAccountVariantTrait, +}; use light_compressed_account::instruction_data::{ cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, }; -use light_sdk::{ - cpi::{v2::CpiAccounts, CpiAccountsConfig, InvokeLightSystemProgram}, - error::LightSdkError, - instruction::{PackedAccounts, PackedAddressTreeInfoExt}, - interface::{ - prepare_compressed_account_on_init, LightAccount, LightAccountVariantTrait, LightFinalize, - LightPreInit, PackedLightAccountVariantTrait, - }, - light_account_checks::packed_accounts::ProgramPackedAccounts, - sdk_types::CpiContextWriteAccounts, -}; -use solana_program_error::ProgramError; use super::{ accounts::{CreateZeroCopy, CreateZeroCopyParams}, @@ -42,113 +36,113 @@ const _: () = { // Manual LightPreInit Implementation // ============================================================================ -impl<'info> LightPreInit<'info, CreateZeroCopyParams> for CreateZeroCopy<'info> { +impl<'info> LightPreInit, CreateZeroCopyParams> for CreateZeroCopy<'info> { fn light_pre_init( &mut self, remaining_accounts: &[AccountInfo<'info>], params: &CreateZeroCopyParams, - ) -> std::result::Result { - use light_sdk::interface::{config::LightConfig, LightAccount}; - use solana_program::{clock::Clock, sysvar::Sysvar}; - use solana_program_error::ProgramError; - - // 1. Build CPI accounts (slice remaining_accounts at system_accounts_offset) - let system_accounts_offset = params.create_accounts_proof.system_accounts_offset as usize; - if remaining_accounts.len() < system_accounts_offset { - return Err(LightSdkError::FewerAccountsThanSystemAccounts); - } - let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); - let cpi_accounts = CpiAccounts::new_with_config( - &self.fee_payer, - &remaining_accounts[system_accounts_offset..], - config, - ); - - // 2. Get address tree pubkey from packed tree info - let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; - let address_tree_pubkey = address_tree_info - .get_tree_pubkey(&cpi_accounts) - .map_err(|_| LightSdkError::from(ProgramError::InvalidAccountData))?; - let output_tree_index = params.create_accounts_proof.output_state_tree_index; - let current_account_index: u8 = 0; - // Is true if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. - const WITH_CPI_CONTEXT: bool = false; - // Is first if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. - let cpi_context = if WITH_CPI_CONTEXT { - CompressedCpiContext::first() - } else { - CompressedCpiContext::default() - }; - const NUM_LIGHT_PDAS: usize = 1; - let mut new_address_params = Vec::with_capacity(NUM_LIGHT_PDAS); - let mut account_infos = Vec::with_capacity(NUM_LIGHT_PDAS); - - // 3. Load config and get current slot - let light_config = LightConfig::load_checked(&self.compression_config, &crate::ID) - .map_err(|_| LightSdkError::from(ProgramError::InvalidAccountData))?; - let current_slot = Clock::get() - .map_err(|_| LightSdkError::from(ProgramError::InvalidAccountData))? - .slot; - - // 4. Prepare compressed account using helper function - // Get the record's key from AccountLoader - let record_key = self.record.key(); - prepare_compressed_account_on_init( - &record_key, - &address_tree_pubkey, - address_tree_info, - output_tree_index, - current_account_index, - &crate::ID, - &mut new_address_params, - &mut account_infos, - )?; - - // 5. Set compression_info on the zero-copy record - // For AccountLoader, we need to use load_init() which was already called by Anchor - { - let mut record = self - .record - .load_init() - .map_err(|_| LightSdkError::from(ProgramError::AccountBorrowFailed))?; - record.set_decompressed(&light_config, current_slot); - } - - // 6. Build instruction data manually (no builder pattern) - let instruction_data = InstructionDataInvokeCpiWithAccountInfo { - mode: 1, // V2 mode - bump: crate::LIGHT_CPI_SIGNER.bump, - invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), - compress_or_decompress_lamports: 0, - is_compress: false, - with_cpi_context: WITH_CPI_CONTEXT, - with_transaction_hash: false, - cpi_context, - proof: params.create_accounts_proof.proof.0, - new_address_params, - account_infos, - read_only_addresses: vec![], - read_only_accounts: vec![], - }; - if !WITH_CPI_CONTEXT { - // 7. Invoke Light System Program CPI - instruction_data - .invoke(cpi_accounts) - .map_err(LightSdkError::from)?; - } else { - // For flows that combine light mints with light PDAs, write to CPI context first. - let cpi_context_accounts = CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts.authority().map_err(LightSdkError::from)?, - cpi_context: cpi_accounts.cpi_context().map_err(LightSdkError::from)?, - cpi_signer: crate::LIGHT_CPI_SIGNER, + ) -> std::result::Result { + let inner = || -> std::result::Result { + use light_account::{LightAccount, LightConfig}; + use solana_program::{clock::Clock, sysvar::Sysvar}; + + // 1. Build CPI accounts (slice remaining_accounts at system_accounts_offset) + let system_accounts_offset = + params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + &self.fee_payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + // 2. Get address tree pubkey from packed tree info + let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&cpi_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let output_tree_index = params.create_accounts_proof.output_state_tree_index; + let current_account_index: u8 = 0; + // Is true if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. + const WITH_CPI_CONTEXT: bool = false; + // Is first if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. + let cpi_context = if WITH_CPI_CONTEXT { + CompressedCpiContext::first() + } else { + CompressedCpiContext::default() }; - instruction_data - .invoke_write_to_cpi_context_first(cpi_context_accounts) - .map_err(LightSdkError::from)?; - } - - Ok(false) // No mints, so no CPI context write + const NUM_LIGHT_PDAS: usize = 1; + let mut new_address_params = Vec::with_capacity(NUM_LIGHT_PDAS); + let mut account_infos = Vec::with_capacity(NUM_LIGHT_PDAS); + + // 3. Load config and get current slot + let light_config = + LightConfig::load_checked(&self.compression_config, &crate::ID.to_bytes()) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let current_slot = Clock::get() + .map_err(|_| LightSdkTypesError::InvalidInstructionData)? + .slot; + + // 4. Prepare compressed account using helper function + // Get the record's key from AccountLoader + let record_key = self.record.key(); + prepare_compressed_account_on_init( + &record_key.to_bytes(), + &address_tree_pubkey.to_bytes(), + address_tree_info, + output_tree_index, + current_account_index, + &crate::ID.to_bytes(), + &mut new_address_params, + &mut account_infos, + )?; + + // 5. Set compression_info on the zero-copy record + // For AccountLoader, we need to use load_init() which was already called by Anchor + { + let mut record = self + .record + .load_init() + .map_err(|_| LightSdkTypesError::Borsh)?; + record.set_decompressed(&light_config, current_slot); + } + + // 6. Build instruction data manually (no builder pattern) + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, // V2 mode + bump: crate::LIGHT_CPI_SIGNER.bump, + invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: WITH_CPI_CONTEXT, + with_transaction_hash: false, + cpi_context, + proof: params.create_accounts_proof.proof.0, + new_address_params, + account_infos, + read_only_addresses: vec![], + read_only_accounts: vec![], + }; + if !WITH_CPI_CONTEXT { + // 7. Invoke Light System Program CPI + instruction_data.invoke(cpi_accounts)?; + } else { + // For flows that combine light mints with light PDAs, write to CPI context first. + let cpi_context_accounts = CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority()?, + cpi_context: cpi_accounts.cpi_context()?, + cpi_signer: crate::LIGHT_CPI_SIGNER, + }; + instruction_data.invoke_write_to_cpi_context_first(cpi_context_accounts)?; + } + + Ok(false) // No mints, so no CPI context write + }; + inner() } } @@ -156,13 +150,13 @@ impl<'info> LightPreInit<'info, CreateZeroCopyParams> for CreateZeroCopy<'info> // Manual LightFinalize Implementation (no-op for PDA-only flow) // ============================================================================ -impl<'info> LightFinalize<'info, CreateZeroCopyParams> for CreateZeroCopy<'info> { +impl<'info> LightFinalize, CreateZeroCopyParams> for CreateZeroCopy<'info> { fn light_finalize( &mut self, _remaining_accounts: &[AccountInfo<'info>], _params: &CreateZeroCopyParams, _has_pre_init: bool, - ) -> std::result::Result<(), LightSdkError> { + ) -> std::result::Result<(), LightSdkTypesError> { // No-op for PDA-only flow - compression CPI already executed in light_pre_init Ok(()) } @@ -213,7 +207,7 @@ pub struct PackedZeroCopyRecordVariant { // ============================================================================ impl LightAccountVariantTrait<4> for ZeroCopyRecordVariant { - const PROGRAM_ID: Pubkey = crate::ID; + const PROGRAM_ID: [u8; 32] = crate::ID.to_bytes(); type Seeds = ZeroCopyRecordSeeds; type Data = ZeroCopyRecord; @@ -252,59 +246,59 @@ impl LightAccountVariantTrait<4> for ZeroCopyRecordVariant { impl PackedLightAccountVariantTrait<4> for PackedZeroCopyRecordVariant { type Unpacked = ZeroCopyRecordVariant; - const ACCOUNT_TYPE: light_sdk::interface::AccountType = - ::ACCOUNT_TYPE; + const ACCOUNT_TYPE: light_account::AccountType = ::ACCOUNT_TYPE; fn bump(&self) -> u8 { self.seeds.bump } - fn unpack(&self, accounts: &[AccountInfo]) -> Result { + fn unpack( + &self, + accounts: &[AI], + ) -> std::result::Result { let owner = accounts .get(self.seeds.owner_idx as usize) - .ok_or(anchor_lang::error::ErrorCode::AccountNotEnoughKeys)?; + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; // Build ProgramPackedAccounts for LightAccount::unpack let packed_accounts = ProgramPackedAccounts { accounts }; let data = ZeroCopyRecord::unpack(&self.data, &packed_accounts) - .map_err(|_| anchor_lang::error::ErrorCode::InvalidProgramId)?; + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; Ok(ZeroCopyRecordVariant { seeds: ZeroCopyRecordSeeds { - owner: *owner.key, + owner: Pubkey::from(owner.key()), name: self.seeds.name.clone(), }, data, }) } - fn seed_refs_with_bump<'a>( + fn seed_refs_with_bump<'a, AI: light_account_checks::AccountInfoTrait>( &'a self, - accounts: &'a [AccountInfo], - bump_storage: &'a [u8; 1], - ) -> std::result::Result<[&'a [u8]; 4], ProgramError> { - let owner = accounts - .get(self.seeds.owner_idx as usize) - .ok_or(ProgramError::InvalidAccountData)?; - Ok([ - b"zero_copy", - owner.key.as_ref(), - self.seeds.name.as_bytes(), - bump_storage, - ]) + _accounts: &'a [AI], + _bump_storage: &'a [u8; 1], + ) -> std::result::Result<[&'a [u8]; 4], LightSdkTypesError> { + Err(LightSdkTypesError::InvalidSeeds) } fn into_in_token_data( &self, - _tree_info: &light_sdk::instruction::PackedStateTreeInfo, + _tree_info: &light_account::PackedStateTreeInfo, _output_queue_index: u8, - ) -> Result { - Err(ProgramError::InvalidAccountData.into()) + ) -> std::result::Result< + light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext, + LightSdkTypesError, + > { + Err(LightSdkTypesError::InvalidInstructionData) } fn into_in_tlv( &self, - ) -> Result>> { + ) -> std::result::Result< + Option>, + LightSdkTypesError, + > { Ok(None) } } @@ -316,19 +310,19 @@ impl PackedLightAccountVariantTrait<4> for PackedZeroCopyRecordVariant { /// Implement IntoVariant to allow building variant from seeds + compressed data. /// This enables the high-level `create_load_instructions` API. #[cfg(not(target_os = "solana"))] -impl light_sdk::interface::IntoVariant for ZeroCopyRecordSeeds { +impl light_account::IntoVariant for ZeroCopyRecordSeeds { fn into_variant( self, data: &[u8], - ) -> std::result::Result { + ) -> std::result::Result { // For ZeroCopy (Pod) accounts, data is the full Pod bytes including compression_info. // We deserialize using AnchorDeserialize (which ZeroCopyRecord implements). let record: ZeroCopyRecord = AnchorDeserialize::deserialize(&mut &data[..]) - .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize)?; + .map_err(|_| LightSdkTypesError::Borsh)?; // Verify the owner in data matches the seed if Pubkey::new_from_array(record.owner) != self.owner { - return Err(anchor_lang::error::ErrorCode::ConstraintSeeds.into()); + return Err(LightSdkTypesError::InvalidSeeds); } Ok(ZeroCopyRecordVariant { @@ -345,19 +339,19 @@ impl light_sdk::interface::IntoVariant for ZeroCopyRecord /// Implement Pack trait to allow ZeroCopyRecordVariant to be used with `create_load_instructions`. /// Transforms the variant into PackedLightAccountVariant for efficient serialization. #[cfg(not(target_os = "solana"))] -impl light_sdk::compressible::Pack for ZeroCopyRecordVariant { +impl light_account::Pack for ZeroCopyRecordVariant { type Packed = crate::derived_variants::PackedLightAccountVariant; fn pack( &self, - accounts: &mut PackedAccounts, - ) -> std::result::Result { - use light_sdk::interface::LightAccountVariantTrait; - let (_, bump) = self.derive_pda(); + accounts: &mut light_account::PackedAccounts, + ) -> std::result::Result { + use light_account::LightAccountVariantTrait; + let (_, bump) = self.derive_pda::(); let packed_data = self .data .pack(accounts) - .map_err(|_| ProgramError::InvalidAccountData)?; + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; Ok( crate::derived_variants::PackedLightAccountVariant::ZeroCopyRecord { seeds: PackedZeroCopyRecordSeeds { diff --git a/sdk-tests/manual-test/src/account_loader/derived_state.rs b/sdk-tests/anchor-manual-test/src/account_loader/derived_state.rs similarity index 67% rename from sdk-tests/manual-test/src/account_loader/derived_state.rs rename to sdk-tests/anchor-manual-test/src/account_loader/derived_state.rs index 1a58462e73..2dfbcf2ed3 100644 --- a/sdk-tests/manual-test/src/account_loader/derived_state.rs +++ b/sdk-tests/anchor-manual-test/src/account_loader/derived_state.rs @@ -4,13 +4,11 @@ //! but for a Pod/zero-copy account type. use anchor_lang::prelude::*; -use light_sdk::{ - compressible::CompressionInfo, - instruction::PackedAccounts, - interface::{AccountType, LightAccount, LightConfig}, +use light_account::{ light_account_checks::{packed_accounts::ProgramPackedAccounts, AccountInfoTrait}, + AccountType, CompressionInfo, HasCompressionInfo, LightAccount, LightConfig, + LightSdkTypesError, }; -use solana_program_error::ProgramError; use super::state::ZeroCopyRecord; @@ -52,13 +50,14 @@ impl LightAccount for ZeroCopyRecord { self.compression_info = CompressionInfo::new_from_config(config, current_slot); } - fn pack( + #[cfg(not(target_os = "solana"))] + fn pack( &self, - accounts: &mut PackedAccounts, - ) -> std::result::Result { + accounts: &mut light_account::interface::instruction::PackedAccounts, + ) -> std::result::Result { // compression_info excluded from packed struct (same as Borsh accounts) Ok(PackedZeroCopyRecord { - owner: accounts.insert_or_get(Pubkey::new_from_array(self.owner)), + owner: accounts.insert_or_get(AM::pubkey_from_bytes(self.owner)), value: self.value, }) } @@ -66,11 +65,11 @@ impl LightAccount for ZeroCopyRecord { fn unpack( packed: &Self::Packed, accounts: &ProgramPackedAccounts, - ) -> std::result::Result { + ) -> std::result::Result { // Use get_u8 with a descriptive name for better error messages let owner_account = accounts .get_u8(packed.owner, "ZeroCopyRecord: owner") - .map_err(|_| ProgramError::InvalidAccountData)?; + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; // Set compression_info to compressed() for hash verification at decompress // (Same pattern as Borsh accounts - canonical compressed state for hashing) @@ -82,3 +81,24 @@ impl LightAccount for ZeroCopyRecord { }) } } + +impl HasCompressionInfo for ZeroCopyRecord { + fn compression_info(&self) -> std::result::Result<&CompressionInfo, LightSdkTypesError> { + Ok(&self.compression_info) + } + + fn compression_info_mut( + &mut self, + ) -> std::result::Result<&mut CompressionInfo, LightSdkTypesError> { + Ok(&mut self.compression_info) + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + panic!("compression_info_mut_opt not supported for LightAccount types (use compression_info_mut instead)") + } + + fn set_compression_info_none(&mut self) -> std::result::Result<(), LightSdkTypesError> { + self.compression_info = CompressionInfo::compressed(); + Ok(()) + } +} diff --git a/sdk-tests/manual-test/src/account_loader/mod.rs b/sdk-tests/anchor-manual-test/src/account_loader/mod.rs similarity index 100% rename from sdk-tests/manual-test/src/account_loader/mod.rs rename to sdk-tests/anchor-manual-test/src/account_loader/mod.rs diff --git a/sdk-tests/manual-test/src/account_loader/state.rs b/sdk-tests/anchor-manual-test/src/account_loader/state.rs similarity index 81% rename from sdk-tests/manual-test/src/account_loader/state.rs rename to sdk-tests/anchor-manual-test/src/account_loader/state.rs index a5e90e08c2..a0ae392da0 100644 --- a/sdk-tests/manual-test/src/account_loader/state.rs +++ b/sdk-tests/anchor-manual-test/src/account_loader/state.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator, LightHasherSha}; +use light_account::{CompressionInfo, Discriminator, LightDiscriminator, LightHasherSha}; /// Zero-copy account for demonstrating AccountLoader integration. /// @@ -10,7 +10,7 @@ use light_sdk::{compressible::CompressionInfo, LightDiscriminator, LightHasherSh /// - `#[repr(C)]` for predictable field layout /// - `Pod + Zeroable` (bytemuck) for on-chain zero-copy access /// - `AnchorSerialize + AnchorDeserialize` for hashing (same as Borsh accounts) -/// - `LightDiscriminator` for dispatch +/// - `Discriminator` for dispatch (matches Anchor's `#[account(zero_copy)]`) /// - compression_info field for rent tracking /// - All fields must be Pod-compatible (no Pubkey, use [u8; 32]) #[derive( @@ -18,8 +18,8 @@ use light_sdk::{compressible::CompressionInfo, LightDiscriminator, LightHasherSh Debug, BorshSerialize, BorshDeserialize, // For hashing (same as Borsh accounts) - LightDiscriminator, - LightHasherSha, // For Light Protocol + Discriminator, // Must use Anchor discriminator since #[account(zero_copy)] is used + LightHasherSha, // For Light Protocol )] #[account(zero_copy)] #[repr(C)] diff --git a/sdk-tests/manual-test/src/all/accounts.rs b/sdk-tests/anchor-manual-test/src/all/accounts.rs similarity index 98% rename from sdk-tests/manual-test/src/all/accounts.rs rename to sdk-tests/anchor-manual-test/src/all/accounts.rs index 5154413813..a72ab92c7c 100644 --- a/sdk-tests/manual-test/src/all/accounts.rs +++ b/sdk-tests/anchor-manual-test/src/all/accounts.rs @@ -1,7 +1,7 @@ //! Accounts module for create_all instruction. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; +use light_account::CreateAccountsProof; use solana_account_info::AccountInfo; use crate::{account_loader::ZeroCopyRecord, pda::MinimalRecord}; diff --git a/sdk-tests/anchor-manual-test/src/all/derived.rs b/sdk-tests/anchor-manual-test/src/all/derived.rs new file mode 100644 index 0000000000..a5bf8b261b --- /dev/null +++ b/sdk-tests/anchor-manual-test/src/all/derived.rs @@ -0,0 +1,269 @@ +//! Derived code for create_all instruction. +//! +//! This implements LightPreInit/LightFinalize for creating all account types: +//! - 2 PDAs (Borsh + ZeroCopy) via `invoke_write_to_cpi_context_first()` +//! - 1 Mint via `invoke_create_mints()` with cpi_context_offset +//! - 1 Token Vault via `CreateTokenAccountCpi` +//! - 1 ATA via `CreateTokenAtaCpi` + +use anchor_lang::prelude::*; +use light_account::{ + derive_associated_token_account, prepare_compressed_account_on_init, CpiAccounts, + CpiAccountsConfig, CpiContextWriteAccounts, CreateMints, CreateMintsStaticAccounts, + CreateTokenAccountCpi, CreateTokenAtaCpi, InvokeLightSystemProgram, LightAccount, + LightFinalize, LightPreInit, LightSdkTypesError, PackedAddressTreeInfoExt, SingleMintParams, +}; +use light_compressed_account::instruction_data::{ + cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, +}; +use solana_account_info::AccountInfo; + +use super::accounts::{ + CreateAllAccounts, CreateAllParams, ALL_MINT_SIGNER_SEED, ALL_TOKEN_VAULT_SEED, +}; + +// ============================================================================ +// LightPreInit Implementation - Creates all accounts at START of instruction +// ============================================================================ + +impl<'info> LightPreInit, CreateAllParams> for CreateAllAccounts<'info> { + fn light_pre_init( + &mut self, + remaining_accounts: &[AccountInfo<'info>], + params: &CreateAllParams, + ) -> std::result::Result { + let mut inner = || -> std::result::Result { + use light_account::LightConfig; + use solana_program::{clock::Clock, sysvar::Sysvar}; + + // Constants for this instruction + const NUM_LIGHT_PDAS: usize = 2; + const NUM_LIGHT_MINTS: usize = 1; + const WITH_CPI_CONTEXT: bool = NUM_LIGHT_PDAS > 0 && NUM_LIGHT_MINTS > 0; // true + + // ==================================================================== + // 1. Build CPI accounts with cpi_context config + // ==================================================================== + let system_accounts_offset = + params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + &self.payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + // ==================================================================== + // 2. Get address tree info + // ==================================================================== + let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&cpi_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let output_tree_index = params.create_accounts_proof.output_state_tree_index; + + // ==================================================================== + // 3. Load config, get current slot + // ==================================================================== + let light_config = + LightConfig::load_checked(&self.compression_config, &crate::ID.to_bytes()) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let current_slot = Clock::get() + .map_err(|_| LightSdkTypesError::InvalidInstructionData)? + .slot; + + // ==================================================================== + // 4. Create PDAs via invoke_write_to_cpi_context_first() + // ==================================================================== + { + // CPI context for PDAs - set to first() since we have mints coming after + let cpi_context = CompressedCpiContext::first(); + let mut new_address_params = Vec::with_capacity(NUM_LIGHT_PDAS); + let mut account_infos = Vec::with_capacity(NUM_LIGHT_PDAS); + + // 4a. Prepare Borsh PDA (index 0) + let borsh_record_key = self.borsh_record.key(); + prepare_compressed_account_on_init( + &borsh_record_key.to_bytes(), + &address_tree_pubkey.to_bytes(), + address_tree_info, + output_tree_index, + 0, // assigned_account_index = 0 + &crate::ID.to_bytes(), + &mut new_address_params, + &mut account_infos, + )?; + self.borsh_record + .set_decompressed(&light_config, current_slot); + + // 4b. Prepare ZeroCopy PDA (index 1) + let zero_copy_record_key = self.zero_copy_record.key(); + prepare_compressed_account_on_init( + &zero_copy_record_key.to_bytes(), + &address_tree_pubkey.to_bytes(), + address_tree_info, + output_tree_index, + 1, // assigned_account_index = 1 + &crate::ID.to_bytes(), + &mut new_address_params, + &mut account_infos, + )?; + { + let mut record = self + .zero_copy_record + .load_init() + .map_err(|_| LightSdkTypesError::Borsh)?; + record.set_decompressed(&light_config, current_slot); + } + + // 4c. Build instruction data and write to CPI context (doesn't execute yet) + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, // V2 mode + bump: crate::LIGHT_CPI_SIGNER.bump, + invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: WITH_CPI_CONTEXT, + with_transaction_hash: false, + cpi_context, + proof: params.create_accounts_proof.proof.0, + new_address_params, + account_infos, + read_only_addresses: vec![], + read_only_accounts: vec![], + }; + + // Write to CPI context first (combined execution happens with mints) + let cpi_context_accounts = CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority()?, + cpi_context: cpi_accounts.cpi_context()?, + cpi_signer: crate::LIGHT_CPI_SIGNER, + }; + instruction_data.invoke_write_to_cpi_context_first(cpi_context_accounts)?; + } + + // ==================================================================== + // 5. Create Mint via CreateMints with cpi_context_offset + // ==================================================================== + { + let authority = self.authority.key(); + let mint_signer_key = self.mint_signer.key(); + + let mint_signer_seeds: &[&[u8]] = &[ + ALL_MINT_SIGNER_SEED, + authority.as_ref(), + &[params.mint_signer_bump], + ]; + + let sdk_mints: [SingleMintParams<'_>; NUM_LIGHT_MINTS] = [SingleMintParams { + decimals: 6, + mint_authority: authority.to_bytes(), + mint_bump: None, + freeze_authority: None, + mint_seed_pubkey: mint_signer_key.to_bytes(), + authority_seeds: None, + mint_signer_seeds: Some(mint_signer_seeds), + token_metadata: None, + }]; + + let payer_info = self.payer.to_account_info(); + let mint_seed_accounts = [self.mint_signer.to_account_info()]; + let mint_accounts = [self.mint.to_account_info()]; + + CreateMints { + mints: &sdk_mints, + proof_data: ¶ms.create_accounts_proof, + mint_seed_accounts: &mint_seed_accounts, + mint_accounts: &mint_accounts, + static_accounts: CreateMintsStaticAccounts { + fee_payer: &payer_info, + compressible_config: &self.compressible_config, + rent_sponsor: &self.rent_sponsor, + cpi_authority: &self.cpi_authority, + }, + cpi_context_offset: NUM_LIGHT_PDAS as u8, + } + .invoke(&cpi_accounts)?; + } + + // ==================================================================== + // 6. Create Token Vault via CreateTokenAccountCpi + // ==================================================================== + { + let mint_key = self.mint.key(); + let vault_seeds: &[&[u8]] = &[ + ALL_TOKEN_VAULT_SEED, + mint_key.as_ref(), + &[params.token_vault_bump], + ]; + + let payer_info = self.payer.to_account_info(); + let token_vault_info = self.token_vault.to_account_info(); + let mint_info = self.mint.to_account_info(); + let system_program_info = self.system_program.to_account_info(); + CreateTokenAccountCpi { + payer: &payer_info, + account: &token_vault_info, + mint: &mint_info, + owner: self.vault_owner.key.to_bytes(), + } + .rent_free( + &self.compressible_config, + &self.rent_sponsor, + &system_program_info, + &crate::ID.to_bytes(), + ) + .invoke_signed(vault_seeds)?; + } + + // ==================================================================== + // 7. Create ATA via CreateTokenAtaCpi + // ==================================================================== + { + let (_, ata_bump) = + derive_associated_token_account(self.ata_owner.key, self.mint.key); + + let payer_info = self.payer.to_account_info(); + let mint_info = self.mint.to_account_info(); + let user_ata_info = self.user_ata.to_account_info(); + let system_program_info = self.system_program.to_account_info(); + CreateTokenAtaCpi { + payer: &payer_info, + owner: &self.ata_owner, + mint: &mint_info, + ata: &user_ata_info, + bump: ata_bump, + } + .rent_free( + &self.compressible_config, + &self.rent_sponsor, + &system_program_info, + ) + .invoke()?; + } + + Ok(WITH_CPI_CONTEXT) + }; + inner() + } +} + +// ============================================================================ +// LightFinalize Implementation - No-op for this flow +// ============================================================================ + +impl<'info> LightFinalize, CreateAllParams> for CreateAllAccounts<'info> { + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo<'info>], + _params: &CreateAllParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + // All accounts were created in light_pre_init + Ok(()) + } +} diff --git a/sdk-tests/manual-test/src/all/derived_accounts.rs b/sdk-tests/anchor-manual-test/src/all/derived_accounts.rs similarity index 70% rename from sdk-tests/manual-test/src/all/derived_accounts.rs rename to sdk-tests/anchor-manual-test/src/all/derived_accounts.rs index 7e69759ef6..704300fb07 100644 --- a/sdk-tests/manual-test/src/all/derived_accounts.rs +++ b/sdk-tests/anchor-manual-test/src/all/derived_accounts.rs @@ -2,12 +2,10 @@ //! Uses different seeds than pda/account_loader modules but reuses the data types. use anchor_lang::prelude::*; -use light_sdk::{ - instruction::PackedAccounts, - interface::{LightAccount, LightAccountVariantTrait, PackedLightAccountVariantTrait}, - light_account_checks::packed_accounts::ProgramPackedAccounts, +use light_account::{ + light_account_checks::{self, packed_accounts::ProgramPackedAccounts}, + LightAccount, LightAccountVariantTrait, PackedLightAccountVariantTrait, }; -use solana_program_error::ProgramError; use super::accounts::{ALL_BORSH_SEED, ALL_ZERO_COPY_SEED}; use crate::{ @@ -56,7 +54,7 @@ pub struct PackedAllBorshVariant { // ============================================================================ impl LightAccountVariantTrait<3> for AllBorshVariant { - const PROGRAM_ID: Pubkey = crate::ID; + const PROGRAM_ID: [u8; 32] = crate::ID.to_bytes(); type Seeds = AllBorshSeeds; type Data = MinimalRecord; @@ -89,52 +87,58 @@ impl LightAccountVariantTrait<3> for AllBorshVariant { impl PackedLightAccountVariantTrait<3> for PackedAllBorshVariant { type Unpacked = AllBorshVariant; - const ACCOUNT_TYPE: light_sdk::interface::AccountType = - ::ACCOUNT_TYPE; + const ACCOUNT_TYPE: light_account::AccountType = ::ACCOUNT_TYPE; fn bump(&self) -> u8 { self.seeds.bump } - fn unpack(&self, accounts: &[AccountInfo]) -> Result { + fn unpack( + &self, + accounts: &[AI], + ) -> std::result::Result { let owner = accounts .get(self.seeds.owner_idx as usize) - .ok_or(anchor_lang::error::ErrorCode::AccountNotEnoughKeys)?; + .ok_or(light_account::LightSdkTypesError::NotEnoughAccountKeys)?; // Build ProgramPackedAccounts for LightAccount::unpack let packed_accounts = ProgramPackedAccounts { accounts }; let data = MinimalRecord::unpack(&self.data, &packed_accounts) - .map_err(|_| anchor_lang::error::ErrorCode::InvalidProgramId)?; + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)?; Ok(AllBorshVariant { - seeds: AllBorshSeeds { owner: *owner.key }, + seeds: AllBorshSeeds { + owner: Pubkey::from(owner.key()), + }, data, }) } - fn seed_refs_with_bump<'a>( + fn seed_refs_with_bump<'a, AI: light_account_checks::AccountInfoTrait>( &'a self, - accounts: &'a [AccountInfo], - bump_storage: &'a [u8; 1], - ) -> std::result::Result<[&'a [u8]; 3], ProgramError> { - let owner = accounts - .get(self.seeds.owner_idx as usize) - .ok_or(ProgramError::InvalidAccountData)?; - Ok([ALL_BORSH_SEED, owner.key.as_ref(), bump_storage]) + _accounts: &'a [AI], + _bump_storage: &'a [u8; 1], + ) -> std::result::Result<[&'a [u8]; 3], light_account::LightSdkTypesError> { + Err(light_account::LightSdkTypesError::InvalidSeeds) } fn into_in_token_data( &self, - _tree_info: &light_sdk::instruction::PackedStateTreeInfo, + _tree_info: &light_account::PackedStateTreeInfo, _output_queue_index: u8, - ) -> anchor_lang::Result { - Err(ProgramError::InvalidAccountData.into()) + ) -> std::result::Result< + light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext, + light_account::LightSdkTypesError, + > { + Err(light_account::LightSdkTypesError::InvalidInstructionData) } fn into_in_tlv( &self, - ) -> anchor_lang::Result>> - { + ) -> std::result::Result< + Option>, + light_account::LightSdkTypesError, + > { Ok(None) } } @@ -180,7 +184,7 @@ pub struct PackedAllZeroCopyVariant { // ============================================================================ impl LightAccountVariantTrait<3> for AllZeroCopyVariant { - const PROGRAM_ID: Pubkey = crate::ID; + const PROGRAM_ID: [u8; 32] = crate::ID.to_bytes(); type Seeds = AllZeroCopySeeds; type Data = ZeroCopyRecord; @@ -213,52 +217,58 @@ impl LightAccountVariantTrait<3> for AllZeroCopyVariant { impl PackedLightAccountVariantTrait<3> for PackedAllZeroCopyVariant { type Unpacked = AllZeroCopyVariant; - const ACCOUNT_TYPE: light_sdk::interface::AccountType = - ::ACCOUNT_TYPE; + const ACCOUNT_TYPE: light_account::AccountType = ::ACCOUNT_TYPE; fn bump(&self) -> u8 { self.seeds.bump } - fn unpack(&self, accounts: &[AccountInfo]) -> Result { + fn unpack( + &self, + accounts: &[AI], + ) -> std::result::Result { let owner = accounts .get(self.seeds.owner_idx as usize) - .ok_or(anchor_lang::error::ErrorCode::AccountNotEnoughKeys)?; + .ok_or(light_account::LightSdkTypesError::NotEnoughAccountKeys)?; // Build ProgramPackedAccounts for LightAccount::unpack let packed_accounts = ProgramPackedAccounts { accounts }; let data = ZeroCopyRecord::unpack(&self.data, &packed_accounts) - .map_err(|_| anchor_lang::error::ErrorCode::InvalidProgramId)?; + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)?; Ok(AllZeroCopyVariant { - seeds: AllZeroCopySeeds { owner: *owner.key }, + seeds: AllZeroCopySeeds { + owner: Pubkey::from(owner.key()), + }, data, }) } - fn seed_refs_with_bump<'a>( + fn seed_refs_with_bump<'a, AI: light_account_checks::AccountInfoTrait>( &'a self, - accounts: &'a [AccountInfo], - bump_storage: &'a [u8; 1], - ) -> std::result::Result<[&'a [u8]; 3], ProgramError> { - let owner = accounts - .get(self.seeds.owner_idx as usize) - .ok_or(ProgramError::InvalidAccountData)?; - Ok([ALL_ZERO_COPY_SEED, owner.key.as_ref(), bump_storage]) + _accounts: &'a [AI], + _bump_storage: &'a [u8; 1], + ) -> std::result::Result<[&'a [u8]; 3], light_account::LightSdkTypesError> { + Err(light_account::LightSdkTypesError::InvalidSeeds) } fn into_in_token_data( &self, - _tree_info: &light_sdk::instruction::PackedStateTreeInfo, + _tree_info: &light_account::PackedStateTreeInfo, _output_queue_index: u8, - ) -> anchor_lang::Result { - Err(ProgramError::InvalidAccountData.into()) + ) -> std::result::Result< + light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext, + light_account::LightSdkTypesError, + > { + Err(light_account::LightSdkTypesError::InvalidInstructionData) } fn into_in_tlv( &self, - ) -> anchor_lang::Result>> - { + ) -> std::result::Result< + Option>, + light_account::LightSdkTypesError, + > { Ok(None) } } @@ -270,18 +280,18 @@ impl PackedLightAccountVariantTrait<3> for PackedAllZeroCopyVariant { /// Implement IntoVariant to allow building variant from seeds + compressed data. /// This enables the high-level `create_load_instructions` API. #[cfg(not(target_os = "solana"))] -impl light_sdk::interface::IntoVariant for AllBorshSeeds { +impl light_account::IntoVariant for AllBorshSeeds { fn into_variant( self, data: &[u8], - ) -> std::result::Result { + ) -> std::result::Result { // Deserialize the compressed data (which includes compression_info) let record: MinimalRecord = AnchorDeserialize::deserialize(&mut &data[..]) - .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize)?; + .map_err(|_| light_account::LightSdkTypesError::Borsh)?; // Verify the owner in data matches the seed if record.owner != self.owner { - return Err(anchor_lang::error::ErrorCode::ConstraintSeeds.into()); + return Err(light_account::LightSdkTypesError::InvalidSeeds); } Ok(AllBorshVariant { @@ -298,19 +308,19 @@ impl light_sdk::interface::IntoVariant for AllBorshSeeds { /// Implement Pack trait to allow AllBorshVariant to be used with `create_load_instructions`. /// Transforms the variant into PackedLightAccountVariant for efficient serialization. #[cfg(not(target_os = "solana"))] -impl light_sdk::compressible::Pack for AllBorshVariant { +impl light_account::Pack for AllBorshVariant { type Packed = crate::derived_variants::PackedLightAccountVariant; fn pack( &self, - accounts: &mut PackedAccounts, - ) -> std::result::Result { - use light_sdk::interface::LightAccountVariantTrait; - let (_, bump) = self.derive_pda(); + accounts: &mut light_account::PackedAccounts, + ) -> std::result::Result { + use light_account::LightAccountVariantTrait; + let (_, bump) = self.derive_pda::(); let packed_data = self .data .pack(accounts) - .map_err(|_| ProgramError::InvalidAccountData)?; + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)?; Ok( crate::derived_variants::PackedLightAccountVariant::AllBorsh { seeds: PackedAllBorshSeeds { @@ -330,19 +340,19 @@ impl light_sdk::compressible::Pack for AllBorshVariant { /// Implement IntoVariant to allow building variant from seeds + compressed data. /// This enables the high-level `create_load_instructions` API. #[cfg(not(target_os = "solana"))] -impl light_sdk::interface::IntoVariant for AllZeroCopySeeds { +impl light_account::IntoVariant for AllZeroCopySeeds { fn into_variant( self, data: &[u8], - ) -> std::result::Result { + ) -> std::result::Result { // For ZeroCopy (Pod) accounts, data is the full Pod bytes including compression_info. // We deserialize using AnchorDeserialize (which ZeroCopyRecord implements). let record: ZeroCopyRecord = AnchorDeserialize::deserialize(&mut &data[..]) - .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize)?; + .map_err(|_| light_account::LightSdkTypesError::Borsh)?; // Verify the owner in data matches the seed if Pubkey::new_from_array(record.owner) != self.owner { - return Err(anchor_lang::error::ErrorCode::ConstraintSeeds.into()); + return Err(light_account::LightSdkTypesError::InvalidSeeds); } Ok(AllZeroCopyVariant { @@ -359,19 +369,19 @@ impl light_sdk::interface::IntoVariant for AllZeroCopySeeds /// Implement Pack trait to allow AllZeroCopyVariant to be used with `create_load_instructions`. /// Transforms the variant into PackedLightAccountVariant for efficient serialization. #[cfg(not(target_os = "solana"))] -impl light_sdk::compressible::Pack for AllZeroCopyVariant { +impl light_account::Pack for AllZeroCopyVariant { type Packed = crate::derived_variants::PackedLightAccountVariant; fn pack( &self, - accounts: &mut PackedAccounts, - ) -> std::result::Result { - use light_sdk::interface::LightAccountVariantTrait; - let (_, bump) = self.derive_pda(); + accounts: &mut light_account::PackedAccounts, + ) -> std::result::Result { + use light_account::LightAccountVariantTrait; + let (_, bump) = self.derive_pda::(); let packed_data = self .data .pack(accounts) - .map_err(|_| ProgramError::InvalidAccountData)?; + .map_err(|_| light_account::LightSdkTypesError::InvalidInstructionData)?; Ok( crate::derived_variants::PackedLightAccountVariant::AllZeroCopy { seeds: PackedAllZeroCopySeeds { diff --git a/sdk-tests/manual-test/src/all/mod.rs b/sdk-tests/anchor-manual-test/src/all/mod.rs similarity index 100% rename from sdk-tests/manual-test/src/all/mod.rs rename to sdk-tests/anchor-manual-test/src/all/mod.rs diff --git a/sdk-tests/manual-test/src/ata/accounts.rs b/sdk-tests/anchor-manual-test/src/ata/accounts.rs similarity index 100% rename from sdk-tests/manual-test/src/ata/accounts.rs rename to sdk-tests/anchor-manual-test/src/ata/accounts.rs diff --git a/sdk-tests/anchor-manual-test/src/ata/derived.rs b/sdk-tests/anchor-manual-test/src/ata/derived.rs new file mode 100644 index 0000000000..62b0183e44 --- /dev/null +++ b/sdk-tests/anchor-manual-test/src/ata/derived.rs @@ -0,0 +1,67 @@ +//! Derived code - what the macro would generate for associated token accounts. + +use anchor_lang::prelude::*; +use light_account::{ + derive_associated_token_account, CreateTokenAtaCpi, LightFinalize, LightPreInit, + LightSdkTypesError, +}; +use solana_account_info::AccountInfo; + +use super::accounts::{CreateAtaAccounts, CreateAtaParams}; + +// ============================================================================ +// LightPreInit Implementation - Creates ATA at START of instruction +// ============================================================================ + +impl<'info> LightPreInit, CreateAtaParams> for CreateAtaAccounts<'info> { + fn light_pre_init( + &mut self, + _remaining_accounts: &[AccountInfo<'info>], + _params: &CreateAtaParams, + ) -> std::result::Result { + let inner = || -> std::result::Result { + // Derive the ATA bump on-chain + let (_, bump) = derive_associated_token_account(self.ata_owner.key, self.mint.key); + + // Create ATA via CPI with idempotent + rent-free mode + // NOTE: Unlike token vaults, ATAs use .invoke() not .invoke_signed() + // because ATAs are derived from [owner, token_program, mint], not program PDAs + let payer_info = self.payer.to_account_info(); + let user_ata_info = self.user_ata.to_account_info(); + let system_program_info = self.system_program.to_account_info(); + CreateTokenAtaCpi { + payer: &payer_info, + owner: &self.ata_owner, + mint: &self.mint, + ata: &user_ata_info, + bump, + } + .idempotent() // Safe: won't fail if ATA already exists + .rent_free( + &self.compressible_config, + &self.rent_sponsor, + &system_program_info, + ) + .invoke()?; + + // ATAs don't use CPI context, return false + Ok(false) + }; + inner() + } +} + +// ============================================================================ +// LightFinalize Implementation - No-op for ATA only flow +// ============================================================================ + +impl<'info> LightFinalize, CreateAtaParams> for CreateAtaAccounts<'info> { + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo<'info>], + _params: &CreateAtaParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + Ok(()) + } +} diff --git a/sdk-tests/manual-test/src/ata/mod.rs b/sdk-tests/anchor-manual-test/src/ata/mod.rs similarity index 100% rename from sdk-tests/manual-test/src/ata/mod.rs rename to sdk-tests/anchor-manual-test/src/ata/mod.rs diff --git a/sdk-tests/manual-test/src/derived_compress.rs b/sdk-tests/anchor-manual-test/src/derived_compress.rs similarity index 85% rename from sdk-tests/manual-test/src/derived_compress.rs rename to sdk-tests/anchor-manual-test/src/derived_compress.rs index 95ae3706bd..1fb092790f 100644 --- a/sdk-tests/manual-test/src/derived_compress.rs +++ b/sdk-tests/anchor-manual-test/src/derived_compress.rs @@ -6,14 +6,11 @@ use std::marker::PhantomData; use anchor_lang::prelude::*; -use light_sdk::{ - interface::{ - prepare_account_for_compression, process_compress_pda_accounts_idempotent, CompressCtx, - }, - LightDiscriminator, +use light_account::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, prepare_account_for_compression, + process_compress_pda_accounts_idempotent, CompressCtx, LightDiscriminator, LightSdkTypesError, }; -use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; -use solana_program_error::ProgramError; +use solana_account_info::AccountInfo; use crate::{account_loader::ZeroCopyRecord, pda::MinimalRecord}; @@ -100,6 +97,7 @@ pub(crate) mod __client_accounts_compress_and_close { pub(crate) mod __cpi_client_accounts_compress_and_close { use super::*; + #[allow(dead_code)] pub struct CompressAndClose<'info>(PhantomData<&'info ()>); impl<'info> anchor_lang::ToAccountMetas for CompressAndClose<'info> { fn to_account_metas( @@ -129,19 +127,21 @@ fn compress_dispatch<'info>( meta: &CompressedAccountMetaNoLamportsNoAddress, index: usize, ctx: &mut CompressCtx<'_, 'info>, -) -> std::result::Result<(), ProgramError> { - let data = account_info.try_borrow_data()?; +) -> std::result::Result<(), LightSdkTypesError> { + let data = account_info + .try_borrow_data() + .map_err(|_| LightSdkTypesError::Borsh)?; // Read discriminator from first 8 bytes let discriminator: [u8; 8] = data[..8] .try_into() - .map_err(|_| ProgramError::InvalidAccountData)?; + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; match discriminator { d if d == MinimalRecord::LIGHT_DISCRIMINATOR => { // Borsh path: deserialize using try_from_slice - let mut account_data = MinimalRecord::try_from_slice(&data[8..]) - .map_err(|_| ProgramError::InvalidAccountData)?; + let mut account_data = + MinimalRecord::try_from_slice(&data[8..]).map_err(|_| LightSdkTypesError::Borsh)?; drop(data); // Call prepare with deserialized data @@ -163,17 +163,19 @@ fn compress_dispatch<'info>( } } -/// MACRO-GENERATED: Process handler - just forwards to SDK function with dispatch. +/// MACRO-GENERATED: Process handler - accepts typed params and forwards to SDK function. pub fn process_compress_and_close<'info>( remaining_accounts: &[AccountInfo<'info>], - instruction_data: &[u8], + params: &light_account::CompressAndCloseParams, ) -> Result<()> { process_compress_pda_accounts_idempotent( remaining_accounts, - instruction_data, + params, compress_dispatch, crate::LIGHT_CPI_SIGNER, - &crate::ID, + &crate::ID.to_bytes(), ) - .map_err(Into::into) + .map_err(|e| { + anchor_lang::error::Error::from(solana_program_error::ProgramError::Custom(u32::from(e))) + }) } diff --git a/sdk-tests/manual-test/src/derived_decompress.rs b/sdk-tests/anchor-manual-test/src/derived_decompress.rs similarity index 86% rename from sdk-tests/manual-test/src/derived_decompress.rs rename to sdk-tests/anchor-manual-test/src/derived_decompress.rs index 1a5aadf599..2e4c2f4069 100644 --- a/sdk-tests/manual-test/src/derived_decompress.rs +++ b/sdk-tests/anchor-manual-test/src/derived_decompress.rs @@ -6,7 +6,7 @@ use std::marker::PhantomData; use anchor_lang::prelude::*; -use light_sdk::interface::process_decompress_pda_accounts_idempotent; +use light_account::process_decompress_pda_accounts_idempotent; use crate::derived_variants::PackedLightAccountVariant; @@ -95,6 +95,7 @@ pub(crate) mod __client_accounts_decompress_idempotent { pub(crate) mod __cpi_client_accounts_decompress_idempotent { use super::*; + #[allow(dead_code)] pub struct DecompressIdempotent<'info>(PhantomData<&'info ()>); impl<'info> anchor_lang::ToAccountMetas for DecompressIdempotent<'info> { fn to_account_metas( @@ -113,16 +114,21 @@ pub(crate) mod __cpi_client_accounts_decompress_idempotent { } } -/// MACRO-GENERATED: Process handler - forwards to SDK function with program's variant type. +/// MACRO-GENERATED: Process handler - accepts typed params and forwards to SDK function. pub fn process_decompress_idempotent<'info>( remaining_accounts: &[AccountInfo<'info>], - instruction_data: &[u8], + params: &light_account::DecompressIdempotentParams, ) -> Result<()> { - process_decompress_pda_accounts_idempotent::( + use solana_program::{clock::Clock, sysvar::Sysvar}; + let current_slot = Clock::get()?.slot; + process_decompress_pda_accounts_idempotent::<_, PackedLightAccountVariant>( remaining_accounts, - instruction_data, + params, crate::LIGHT_CPI_SIGNER, - &crate::ID, + &crate::ID.to_bytes(), + current_slot, ) - .map_err(Into::into) + .map_err(|e| { + anchor_lang::error::Error::from(solana_program_error::ProgramError::Custom(u32::from(e))) + }) } diff --git a/sdk-tests/manual-test/src/derived_light_config.rs b/sdk-tests/anchor-manual-test/src/derived_light_config.rs similarity index 63% rename from sdk-tests/manual-test/src/derived_light_config.rs rename to sdk-tests/anchor-manual-test/src/derived_light_config.rs index babceee774..49157a5037 100644 --- a/sdk-tests/manual-test/src/derived_light_config.rs +++ b/sdk-tests/anchor-manual-test/src/derived_light_config.rs @@ -1,8 +1,9 @@ //! Config instructions using SDK functions. use anchor_lang::prelude::*; +use light_account::process_initialize_light_config; use light_compressible::rent::RentConfig; -use light_sdk::interface::config::{process_initialize_light_config, process_update_light_config}; +use solana_program_error::ProgramError; /// Params order matches SDK's InitializeCompressionConfigAnchorData. #[derive(AnchorSerialize, AnchorDeserialize, Clone)] @@ -41,27 +42,17 @@ pub fn process_initialize_config<'info>( process_initialize_light_config( &ctx.accounts.config, &ctx.accounts.authority, - ¶ms.rent_sponsor, - ¶ms.compression_authority, + ¶ms.rent_sponsor.to_bytes(), + ¶ms.compression_authority.to_bytes(), params.rent_config, params.write_top_up, - params.address_space, + params.address_space.iter().map(|p| p.to_bytes()).collect(), 0, // config_bump &ctx.accounts.fee_payer, &ctx.accounts.system_program, - &crate::ID, + &crate::ID.to_bytes(), ) - .map_err(Into::into) -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct UpdateConfigParams { - pub new_update_authority: Option, - pub new_rent_sponsor: Option, - pub new_compression_authority: Option, - pub new_rent_config: Option, - pub new_write_top_up: Option, - pub new_address_space: Option>, + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e)))) } #[derive(Accounts)] @@ -76,18 +67,12 @@ pub struct UpdateConfig<'info> { pub fn process_update_config<'info>( ctx: Context<'_, '_, '_, 'info, UpdateConfig<'info>>, - params: UpdateConfigParams, + instruction_data: Vec, ) -> Result<()> { - process_update_light_config( - &ctx.accounts.config, - &ctx.accounts.authority, - params.new_update_authority.as_ref(), - params.new_rent_sponsor.as_ref(), - params.new_compression_authority.as_ref(), - params.new_rent_config, - params.new_write_top_up, - params.new_address_space, - &crate::ID, - ) - .map_err(Into::into) + let remaining = [ + ctx.accounts.config.to_account_info(), + ctx.accounts.authority.to_account_info(), + ]; + light_account::process_update_light_config(&remaining, &instruction_data, &crate::ID.to_bytes()) + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e)))) } diff --git a/sdk-tests/manual-test/src/derived_variants.rs b/sdk-tests/anchor-manual-test/src/derived_variants.rs similarity index 88% rename from sdk-tests/manual-test/src/derived_variants.rs rename to sdk-tests/anchor-manual-test/src/derived_variants.rs index cf02bb3ce3..31ff7253d4 100644 --- a/sdk-tests/manual-test/src/derived_variants.rs +++ b/sdk-tests/anchor-manual-test/src/derived_variants.rs @@ -3,9 +3,11 @@ //! This module contains the code that would be generated by the `#[light_program]` macro. use anchor_lang::prelude::*; -use light_sdk::interface::{prepare_account_for_decompression, DecompressCtx, DecompressVariant}; -use light_sdk_types::instruction::PackedStateTreeInfo; -use solana_program_error::ProgramError; +use light_account::{ + prepare_account_for_decompression, DecompressCtx, DecompressVariant, LightSdkTypesError, + PackedStateTreeInfo, +}; +use solana_account_info::AccountInfo; use crate::{ account_loader::derived_accounts::{ @@ -75,13 +77,13 @@ pub enum PackedLightAccountVariant { /// Implementation for PackedLightAccountVariant. /// Implements on the inner variant type to satisfy orphan rules. -impl<'info> DecompressVariant<'info> for PackedLightAccountVariant { +impl<'info> DecompressVariant> for PackedLightAccountVariant { fn decompress( &self, tree_info: &PackedStateTreeInfo, pda_account: &AccountInfo<'info>, ctx: &mut DecompressCtx<'_, 'info>, - ) -> std::result::Result<(), ProgramError> { + ) -> std::result::Result<(), LightSdkTypesError> { let output_queue_index = ctx.output_queue_index; match self { PackedLightAccountVariant::MinimalRecord { seeds, data } => { @@ -89,7 +91,7 @@ impl<'info> DecompressVariant<'info> for PackedLightAccountVariant { seeds: seeds.clone(), data: data.clone(), }; - prepare_account_for_decompression::<4, PackedMinimalRecordVariant>( + prepare_account_for_decompression::<4, PackedMinimalRecordVariant, AccountInfo<'info>>( &packed_data, tree_info, output_queue_index, @@ -102,7 +104,11 @@ impl<'info> DecompressVariant<'info> for PackedLightAccountVariant { seeds: seeds.clone(), data: data.clone(), }; - prepare_account_for_decompression::<4, PackedZeroCopyRecordVariant>( + prepare_account_for_decompression::< + 4, + PackedZeroCopyRecordVariant, + AccountInfo<'info>, + >( &packed_data, tree_info, output_queue_index, @@ -115,7 +121,7 @@ impl<'info> DecompressVariant<'info> for PackedLightAccountVariant { seeds: seeds.clone(), data: data.clone(), }; - prepare_account_for_decompression::<3, PackedAllBorshVariant>( + prepare_account_for_decompression::<3, PackedAllBorshVariant, AccountInfo<'info>>( &packed_data, tree_info, output_queue_index, @@ -128,7 +134,7 @@ impl<'info> DecompressVariant<'info> for PackedLightAccountVariant { seeds: seeds.clone(), data: data.clone(), }; - prepare_account_for_decompression::<3, PackedAllZeroCopyVariant>( + prepare_account_for_decompression::<3, PackedAllZeroCopyVariant, AccountInfo<'info>>( &packed_data, tree_info, output_queue_index, diff --git a/sdk-tests/manual-test/src/lib.rs b/sdk-tests/anchor-manual-test/src/lib.rs similarity index 92% rename from sdk-tests/manual-test/src/lib.rs rename to sdk-tests/anchor-manual-test/src/lib.rs index a3c2366b27..24c97afd18 100644 --- a/sdk-tests/manual-test/src/lib.rs +++ b/sdk-tests/anchor-manual-test/src/lib.rs @@ -9,11 +9,7 @@ #![allow(deprecated)] use anchor_lang::prelude::*; -use light_sdk::{ - derive_light_cpi_signer, - interface::{LightFinalize, LightPreInit}, -}; -use light_sdk_types::CpiSigner; +use light_account::{derive_light_cpi_signer, CpiSigner, LightFinalize, LightPreInit}; use solana_program_error::ProgramError; pub mod account_loader; @@ -41,7 +37,7 @@ pub use derived_compress::*; pub use derived_decompress::*; pub use derived_light_config::*; pub use derived_variants::{LightAccountVariant, PackedLightAccountVariant}; -pub use light_sdk::interface::{ +pub use light_account::{ AccountType, CompressAndCloseParams, DecompressIdempotentParams, DecompressVariant, LightAccount, }; @@ -74,7 +70,7 @@ pub mod manual_test { let has_pre_init = ctx .accounts .light_pre_init(ctx.remaining_accounts, ¶ms) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; // 2. Business logic: set account data ctx.accounts.record.owner = params.owner; @@ -82,7 +78,7 @@ pub mod manual_test { // 3. Finalize: no-op for PDA-only flow ctx.accounts .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; Ok(()) } @@ -100,33 +96,33 @@ pub mod manual_test { /// Named to match SDK's expected discriminator. pub fn update_compression_config<'info>( ctx: Context<'_, '_, '_, 'info, UpdateConfig<'info>>, - params: UpdateConfigParams, + instruction_data: Vec, ) -> Result<()> { - derived_light_config::process_update_config(ctx, params) + derived_light_config::process_update_config(ctx, instruction_data) } /// Compress and close PDA accounts, returning rent to the sponsor. /// Named to match SDK/forester expected discriminator. /// /// NOTE: Empty Accounts struct - everything in remaining_accounts. - /// Deserialization happens inside process_compress_pda_accounts_idempotent. + /// Anchor deserializes typed params directly. pub fn compress_accounts_idempotent<'info>( ctx: Context<'_, '_, '_, 'info, CompressAndClose<'info>>, - instruction_data: Vec, + params: CompressAndCloseParams, ) -> Result<()> { - derived_compress::process_compress_and_close(ctx.remaining_accounts, &instruction_data) + derived_compress::process_compress_and_close(ctx.remaining_accounts, ¶ms) } /// Decompress compressed accounts back into PDAs idempotently. /// Named to match SDK expected discriminator. /// /// NOTE: PhantomData struct - all accounts in remaining_accounts. - /// Deserialization happens inside process_decompress_pda_accounts_idempotent. + /// Anchor deserializes typed params directly. pub fn decompress_accounts_idempotent<'info>( ctx: Context<'_, '_, '_, 'info, DecompressIdempotent<'info>>, - instruction_data: Vec, + params: DecompressIdempotentParams, ) -> Result<()> { - derived_decompress::process_decompress_idempotent(ctx.remaining_accounts, &instruction_data) + derived_decompress::process_decompress_idempotent(ctx.remaining_accounts, ¶ms) } /// Create a single zero-copy compressible PDA using AccountLoader. @@ -140,7 +136,7 @@ pub mod manual_test { let has_pre_init = ctx .accounts .light_pre_init(ctx.remaining_accounts, ¶ms) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; // 2. Business logic: set account data using load_init() pattern { @@ -152,7 +148,7 @@ pub mod manual_test { // 3. Finalize: no-op for PDA-only flow ctx.accounts .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; Ok(()) } @@ -168,14 +164,14 @@ pub mod manual_test { let has_pre_init = ctx .accounts .light_pre_init(ctx.remaining_accounts, ¶ms) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; // 2. No business logic for mint-only creation // 3. Finalize: no-op for mint-only flow ctx.accounts .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; Ok(()) } @@ -191,14 +187,14 @@ pub mod manual_test { let has_pre_init = ctx .accounts .light_pre_init(ctx.remaining_accounts, ¶ms) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; // 2. No business logic for token vault-only creation // 3. Finalize: no-op for token vault-only flow ctx.accounts .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; Ok(()) } @@ -214,14 +210,14 @@ pub mod manual_test { let has_pre_init = ctx .accounts .light_pre_init(ctx.remaining_accounts, ¶ms) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; // 2. No business logic for ATA-only creation // 3. Finalize: no-op for ATA-only flow ctx.accounts .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; Ok(()) } @@ -243,7 +239,7 @@ pub mod manual_test { let has_pre_init = ctx .accounts .light_pre_init(ctx.remaining_accounts, ¶ms) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; // 2. Business logic: set PDA data ctx.accounts.borsh_record.owner = params.owner; @@ -256,7 +252,7 @@ pub mod manual_test { // 3. Finalize: no-op for this flow ctx.accounts .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) - .map_err(|e| anchor_lang::error::Error::from(ProgramError::from(e)))?; + .map_err(|e| anchor_lang::error::Error::from(ProgramError::Custom(u32::from(e))))?; Ok(()) } diff --git a/sdk-tests/manual-test/src/pda/accounts.rs b/sdk-tests/anchor-manual-test/src/pda/accounts.rs similarity index 95% rename from sdk-tests/manual-test/src/pda/accounts.rs rename to sdk-tests/anchor-manual-test/src/pda/accounts.rs index 62b05baa18..8acb913177 100644 --- a/sdk-tests/manual-test/src/pda/accounts.rs +++ b/sdk-tests/anchor-manual-test/src/pda/accounts.rs @@ -1,7 +1,7 @@ //! Accounts module for single-pda-test. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; +use light_account::CreateAccountsProof; use crate::pda::MinimalRecord; diff --git a/sdk-tests/anchor-manual-test/src/pda/derived_accounts.rs b/sdk-tests/anchor-manual-test/src/pda/derived_accounts.rs new file mode 100644 index 0000000000..d60736fea6 --- /dev/null +++ b/sdk-tests/anchor-manual-test/src/pda/derived_accounts.rs @@ -0,0 +1,365 @@ +use anchor_lang::prelude::*; +use light_account::{ + light_account_checks::{self, packed_accounts::ProgramPackedAccounts}, + prepare_compressed_account_on_init, CpiAccounts, CpiAccountsConfig, CpiContextWriteAccounts, + InvokeLightSystemProgram, LightAccount, LightAccountVariantTrait, LightFinalize, LightPreInit, + LightSdkTypesError, PackedAddressTreeInfoExt, PackedLightAccountVariantTrait, +}; +use light_compressed_account::instruction_data::{ + cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, +}; + +use super::{ + accounts::{CreatePda, CreatePdaParams}, + derived_state::PackedMinimalRecord, + state::MinimalRecord, +}; + +// ============================================================================ +// Compile-time Size Validation (800-byte limit for compressed accounts) +// ============================================================================ + +const _: () = { + // Use Anchor's Space trait (from #[derive(InitSpace)]) + const COMPRESSED_SIZE: usize = 8 + ::INIT_SPACE; + assert!( + COMPRESSED_SIZE <= 800, + "Compressed account 'MinimalRecord' exceeds 800-byte compressible account size limit" + ); +}; + +// ============================================================================ +// Manual LightPreInit Implementation +// ============================================================================ + +impl<'info> LightPreInit, CreatePdaParams> for CreatePda<'info> { + fn light_pre_init( + &mut self, + remaining_accounts: &[AccountInfo<'info>], + params: &CreatePdaParams, + ) -> std::result::Result { + let mut inner = || -> std::result::Result { + use light_account::{LightAccount, LightConfig}; + use solana_program::{clock::Clock, sysvar::Sysvar}; + + // 1. Build CPI accounts (slice remaining_accounts at system_accounts_offset) + let system_accounts_offset = + params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + &self.fee_payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + // 2. Get address tree pubkey from packed tree info + let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&cpi_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let output_tree_index = params.create_accounts_proof.output_state_tree_index; + let current_account_index: u8 = 0; + // Is true if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. + const WITH_CPI_CONTEXT: bool = false; + + const NUM_LIGHT_PDAS: usize = 1; + + // 6. Set compression_info from config + let light_config = + LightConfig::load_checked(&self.compression_config, &crate::ID.to_bytes()) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let current_slot = Clock::get() + .map_err(|_| LightSdkTypesError::InvalidInstructionData)? + .slot; + // Dynamic derived light pda specific. Only exists if NUM_LIGHT_PDAS > 0 + // ===================================================================== + { + // Is first if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. + let cpi_context = if WITH_CPI_CONTEXT { + CompressedCpiContext::first() + } else { + CompressedCpiContext::default() + }; + let mut new_address_params = Vec::with_capacity(NUM_LIGHT_PDAS); + let mut account_infos = Vec::with_capacity(NUM_LIGHT_PDAS); + // 3. Prepare compressed account using helper function + // Dynamic code 0-N variants depending on the accounts struct + // ===================================================================== + prepare_compressed_account_on_init( + &self.record.key().to_bytes(), + &address_tree_pubkey.to_bytes(), + address_tree_info, + output_tree_index, + current_account_index, + &crate::ID.to_bytes(), + &mut new_address_params, + &mut account_infos, + )?; + self.record.set_decompressed(&light_config, current_slot); + // ===================================================================== + + // current_account_index += 1; + // For multiple accounts, repeat the pattern: + // let prepared2 = prepare_compressed_account_on_init(..., current_account_index, ...)?; + // current_account_index += 1; + + // 4. Build instruction data manually (no builder pattern) + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, // V2 mode + bump: crate::LIGHT_CPI_SIGNER.bump, + invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: WITH_CPI_CONTEXT, + with_transaction_hash: false, + cpi_context, + proof: params.create_accounts_proof.proof.0, + new_address_params, + account_infos, + read_only_addresses: vec![], + read_only_accounts: vec![], + }; + if !WITH_CPI_CONTEXT { + // 5. Invoke Light System Program CPI + instruction_data.invoke(cpi_accounts)?; + } else { + // For flows that combine light mints with light PDAs, write to CPI context first. + // The authority and cpi_context accounts must be provided in remaining_accounts. + let cpi_context_accounts = CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority()?, + cpi_context: cpi_accounts.cpi_context()?, + cpi_signer: crate::LIGHT_CPI_SIGNER, + }; + instruction_data.invoke_write_to_cpi_context_first(cpi_context_accounts)?; + } + } + // ===================================================================== + Ok(false) // No mints, so no CPI context write + }; + inner() + } +} + +// ============================================================================ +// Manual LightFinalize Implementation (no-op for PDA-only flow) +// ============================================================================ + +impl<'info> LightFinalize, CreatePdaParams> for CreatePda<'info> { + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo<'info>], + _params: &CreatePdaParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + // No-op for PDA-only flow - compression CPI already executed in light_pre_init + Ok(()) + } +} + +// ============================================================================ +// Seeds Structs +// Extracted from: seeds = [b"minimal_record", params.owner.as_ref()] +// ============================================================================ + +/// Seeds for MinimalRecord PDA. +/// Contains the dynamic seed values (static prefix "minimal_record" is in seed_refs). +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct MinimalRecordSeeds { + pub owner: Pubkey, + pub nonce: u64, +} + +/// Packed seeds with u8 indices instead of Pubkeys. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct PackedMinimalRecordSeeds { + pub owner_idx: u8, + pub nonce_bytes: [u8; 8], + pub bump: u8, +} + +// ============================================================================ +// Variant Structs +// ============================================================================ + +/// Full variant combining seeds + data. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct MinimalRecordVariant { + pub seeds: MinimalRecordSeeds, + pub data: MinimalRecord, +} + +/// Packed variant for efficient serialization. +/// Contains packed seeds and data with u8 indices for Pubkey deduplication. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct PackedMinimalRecordVariant { + pub seeds: PackedMinimalRecordSeeds, + pub data: PackedMinimalRecord, +} + +// ============================================================================ +// LightAccountVariant Implementation +// ============================================================================ + +impl LightAccountVariantTrait<4> for MinimalRecordVariant { + const PROGRAM_ID: [u8; 32] = crate::ID.to_bytes(); + + type Seeds = MinimalRecordSeeds; + type Data = MinimalRecord; + type Packed = PackedMinimalRecordVariant; + + fn data(&self) -> &Self::Data { + &self.data + } + + /// Get seed values as owned byte vectors for PDA derivation. + /// Generated from: seeds = [b"minimal_record", params.owner.as_ref(), ¶ms.nonce.to_le_bytes()] + fn seed_vec(&self) -> Vec> { + vec![ + b"minimal_record".to_vec(), + self.seeds.owner.to_bytes().to_vec(), + self.seeds.nonce.to_le_bytes().to_vec(), + ] + } + + /// Get seed references with bump for CPI signing. + /// Note: For unpacked variants with computed bytes (like nonce.to_le_bytes()), + /// we cannot return references to temporaries. Use the packed variant instead. + fn seed_refs_with_bump<'a>(&'a self, _bump_storage: &'a [u8; 1]) -> [&'a [u8]; 4] { + // The packed variant stores nonce_bytes as [u8; 8], so it can return references. + // This unpacked variant computes nonce.to_le_bytes() which creates a temporary. + panic!("Use PackedMinimalRecordVariant::seed_refs_with_bump instead") + } +} + +// ============================================================================ +// PackedLightAccountVariant Implementation +// ============================================================================ + +impl PackedLightAccountVariantTrait<4> for PackedMinimalRecordVariant { + type Unpacked = MinimalRecordVariant; + + const ACCOUNT_TYPE: light_account::AccountType = ::ACCOUNT_TYPE; + + fn bump(&self) -> u8 { + self.seeds.bump + } + + fn unpack( + &self, + accounts: &[AI], + ) -> std::result::Result { + let owner = accounts + .get(self.seeds.owner_idx as usize) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + + // Build ProgramPackedAccounts for LightAccount::unpack + let packed_accounts = ProgramPackedAccounts { accounts }; + let data = MinimalRecord::unpack(&self.data, &packed_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + + Ok(MinimalRecordVariant { + seeds: MinimalRecordSeeds { + owner: Pubkey::from(owner.key()), + nonce: u64::from_le_bytes(self.seeds.nonce_bytes), + }, + data, + }) + } + + fn seed_refs_with_bump<'a, AI: light_account_checks::AccountInfoTrait>( + &'a self, + _accounts: &'a [AI], + _bump_storage: &'a [u8; 1], + ) -> std::result::Result<[&'a [u8]; 4], LightSdkTypesError> { + // PDA variants use seed_vec() in the decompression path, not seed_refs_with_bump. + // Returning a reference to the account key requires a key_ref() method on + // AccountInfoTrait, which is not yet available. Since this method is only + // called for token account variants, PDA variants return an error. + Err(LightSdkTypesError::InvalidSeeds) + } + + fn into_in_token_data( + &self, + _tree_info: &light_account::PackedStateTreeInfo, + _output_queue_index: u8, + ) -> std::result::Result< + light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext, + LightSdkTypesError, + > { + Err(LightSdkTypesError::InvalidInstructionData) + } + + fn into_in_tlv( + &self, + ) -> std::result::Result< + Option>, + LightSdkTypesError, + > { + Ok(None) + } +} + +// ============================================================================ +// IntoVariant Implementation for Seeds (client-side API) +// ============================================================================ + +/// Implement IntoVariant to allow building variant from seeds + compressed data. +/// This enables the high-level `create_load_instructions` API. +#[cfg(not(target_os = "solana"))] +impl light_account::IntoVariant for MinimalRecordSeeds { + fn into_variant( + self, + data: &[u8], + ) -> std::result::Result { + // Deserialize the compressed data (which includes compression_info) + let record: MinimalRecord = AnchorDeserialize::deserialize(&mut &data[..]) + .map_err(|_| LightSdkTypesError::Borsh)?; + + // Verify the owner in data matches the seed + if record.owner != self.owner { + return Err(LightSdkTypesError::InvalidSeeds); + } + + Ok(MinimalRecordVariant { + seeds: self, + data: record, + }) + } +} + +// ============================================================================ +// Pack Implementation for MinimalRecordVariant (client-side API) +// ============================================================================ + +/// Implement Pack trait to allow MinimalRecordVariant to be used with `create_load_instructions`. +/// Transforms the variant into PackedLightAccountVariant for efficient serialization. +#[cfg(not(target_os = "solana"))] +impl light_account::Pack for MinimalRecordVariant { + type Packed = crate::derived_variants::PackedLightAccountVariant; + + fn pack( + &self, + accounts: &mut light_account::PackedAccounts, + ) -> std::result::Result { + use light_account::LightAccountVariantTrait; + let (_, bump) = self.derive_pda::(); + let packed_data = self + .data + .pack(accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + Ok( + crate::derived_variants::PackedLightAccountVariant::MinimalRecord { + seeds: PackedMinimalRecordSeeds { + owner_idx: accounts.insert_or_get(self.seeds.owner), + nonce_bytes: self.seeds.nonce.to_le_bytes(), + bump, + }, + data: packed_data, + }, + ) + } +} diff --git a/sdk-tests/manual-test/src/pda/derived_state.rs b/sdk-tests/anchor-manual-test/src/pda/derived_state.rs similarity index 60% rename from sdk-tests/manual-test/src/pda/derived_state.rs rename to sdk-tests/anchor-manual-test/src/pda/derived_state.rs index e8d9b05a64..7b17b2c604 100644 --- a/sdk-tests/manual-test/src/pda/derived_state.rs +++ b/sdk-tests/anchor-manual-test/src/pda/derived_state.rs @@ -1,11 +1,9 @@ use anchor_lang::prelude::*; -use light_sdk::{ - compressible::CompressionInfo, - instruction::PackedAccounts, - interface::{AccountType, LightAccount, LightConfig}, +use light_account::{ light_account_checks::{packed_accounts::ProgramPackedAccounts, AccountInfoTrait}, + AccountType, CompressionInfo, HasCompressionInfo, LightAccount, LightConfig, + LightSdkTypesError, }; -use solana_program_error::ProgramError; use super::state::MinimalRecord; @@ -31,7 +29,7 @@ impl LightAccount for MinimalRecord { type Packed = PackedMinimalRecord; // CompressionInfo (24) + Pubkey (32) = 56 bytes - const INIT_SPACE: usize = CompressionInfo::INIT_SPACE + 32; + const INIT_SPACE: usize = core::mem::size_of::() + 32; fn compression_info(&self) -> &CompressionInfo { &self.compression_info @@ -45,24 +43,25 @@ impl LightAccount for MinimalRecord { self.compression_info = CompressionInfo::new_from_config(config, current_slot); } - fn pack( + #[cfg(not(target_os = "solana"))] + fn pack( &self, - accounts: &mut PackedAccounts, - ) -> std::result::Result { + accounts: &mut light_account::interface::instruction::PackedAccounts, + ) -> std::result::Result { // compression_info excluded from packed struct Ok(PackedMinimalRecord { - owner: accounts.insert_or_get(self.owner), + owner: accounts.insert_or_get(AM::pubkey_from_bytes(self.owner.to_bytes())), }) } fn unpack( packed: &Self::Packed, accounts: &ProgramPackedAccounts, - ) -> std::result::Result { + ) -> std::result::Result { // Use get_u8 with a descriptive name for better error messages let owner_account = accounts .get_u8(packed.owner, "MinimalRecord: owner") - .map_err(|_| ProgramError::InvalidAccountData)?; + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; // Set compression_info to compressed() for hash verification at decompress Ok(MinimalRecord { @@ -71,3 +70,24 @@ impl LightAccount for MinimalRecord { }) } } + +impl HasCompressionInfo for MinimalRecord { + fn compression_info(&self) -> std::result::Result<&CompressionInfo, LightSdkTypesError> { + Ok(&self.compression_info) + } + + fn compression_info_mut( + &mut self, + ) -> std::result::Result<&mut CompressionInfo, LightSdkTypesError> { + Ok(&mut self.compression_info) + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + panic!("compression_info_mut_opt not supported for LightAccount types (use compression_info_mut instead)") + } + + fn set_compression_info_none(&mut self) -> std::result::Result<(), LightSdkTypesError> { + self.compression_info = CompressionInfo::compressed(); + Ok(()) + } +} diff --git a/sdk-tests/manual-test/src/pda/mod.rs b/sdk-tests/anchor-manual-test/src/pda/mod.rs similarity index 100% rename from sdk-tests/manual-test/src/pda/mod.rs rename to sdk-tests/anchor-manual-test/src/pda/mod.rs diff --git a/sdk-tests/manual-test/src/pda/state.rs b/sdk-tests/anchor-manual-test/src/pda/state.rs similarity index 62% rename from sdk-tests/manual-test/src/pda/state.rs rename to sdk-tests/anchor-manual-test/src/pda/state.rs index a94f3f2785..478cb9db44 100644 --- a/sdk-tests/manual-test/src/pda/state.rs +++ b/sdk-tests/anchor-manual-test/src/pda/state.rs @@ -1,7 +1,7 @@ //! State module for single-pda-test. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator, LightHasherSha}; +use light_account::{CompressionInfo, Discriminator, LightDiscriminator, LightHasherSha}; // ============================================================================ // MinimalRecord with derive macros @@ -10,9 +10,13 @@ use light_sdk::{compressible::CompressionInfo, LightDiscriminator, LightHasherSh /// Minimal record struct for testing PDA creation. /// Contains only compression_info and one field. /// -#[derive(Default, Debug, InitSpace, LightDiscriminator, LightHasherSha)] // LightAccount +#[derive(Default, Debug, Discriminator, LightHasherSha)] // LightAccount #[account] pub struct MinimalRecord { pub compression_info: CompressionInfo, pub owner: Pubkey, } + +impl anchor_lang::Space for MinimalRecord { + const INIT_SPACE: usize = core::mem::size_of::() + 32; +} diff --git a/sdk-tests/manual-test/src/token_account/accounts.rs b/sdk-tests/anchor-manual-test/src/token_account/accounts.rs similarity index 100% rename from sdk-tests/manual-test/src/token_account/accounts.rs rename to sdk-tests/anchor-manual-test/src/token_account/accounts.rs diff --git a/sdk-tests/anchor-manual-test/src/token_account/derived.rs b/sdk-tests/anchor-manual-test/src/token_account/derived.rs new file mode 100644 index 0000000000..7f8804c0f4 --- /dev/null +++ b/sdk-tests/anchor-manual-test/src/token_account/derived.rs @@ -0,0 +1,117 @@ +//! Derived code - what the macro would generate for token accounts. + +use anchor_lang::prelude::*; +#[cfg(not(target_os = "solana"))] +use light_account::Pack; +use light_account::{ + CreateTokenAccountCpi, LightFinalize, LightPreInit, LightSdkTypesError, Unpack, +}; +use solana_account_info::AccountInfo; + +use super::accounts::{CreateTokenVaultAccounts, CreateTokenVaultParams, TOKEN_VAULT_SEED}; + +// ============================================================================ +// LightPreInit Implementation - Creates token account at START of instruction +// ============================================================================ + +impl<'info> LightPreInit, CreateTokenVaultParams> + for CreateTokenVaultAccounts<'info> +{ + fn light_pre_init( + &mut self, + _remaining_accounts: &[AccountInfo<'info>], + params: &CreateTokenVaultParams, + ) -> std::result::Result { + let inner = || -> std::result::Result { + // Build PDA seeds: [TOKEN_VAULT_SEED, mint.key(), &[bump]] + let mint_key = self.mint.key(); + let vault_seeds: &[&[u8]] = + &[TOKEN_VAULT_SEED, mint_key.as_ref(), &[params.vault_bump]]; + + // Create token account via CPI with rent-free mode + let payer_info = self.payer.to_account_info(); + let token_vault_info = self.token_vault.to_account_info(); + let system_program_info = self.system_program.to_account_info(); + CreateTokenAccountCpi { + payer: &payer_info, + account: &token_vault_info, + mint: &self.mint, + owner: self.vault_owner.key.to_bytes(), + } + .rent_free( + &self.compressible_config, + &self.rent_sponsor, + &system_program_info, + &crate::ID.to_bytes(), + ) + .invoke_signed(vault_seeds)?; + + // Token accounts don't use CPI context, return false + Ok(false) + }; + inner() + } +} + +// ============================================================================ +// LightFinalize Implementation - No-op for token account only flow +// ============================================================================ + +impl<'info> LightFinalize, CreateTokenVaultParams> + for CreateTokenVaultAccounts<'info> +{ + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo<'info>], + _params: &CreateTokenVaultParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + Ok(()) + } +} +/* inside of in_tlv for (i, token) in params.token_accounts.iter().enumerate() { + if let Some(extension) = token.extension.clone() { + vec[i] = Some(vec![ExtensionInstructionData::CompressedOnly(extension)]); + } +}*/ +#[allow(dead_code)] +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct TokenVaultSeeds { + pub mint: Pubkey, +} + +#[cfg(not(target_os = "solana"))] +impl Pack for TokenVaultSeeds { + type Packed = PackedTokenVaultSeeds; + fn pack( + &self, + remaining_accounts: &mut light_account::PackedAccounts, + ) -> std::result::Result { + Ok(PackedTokenVaultSeeds { + mint_idx: remaining_accounts.insert_or_get(self.mint), + bump: 0, + }) + } +} + +#[allow(dead_code)] +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct PackedTokenVaultSeeds { + pub mint_idx: u8, + pub bump: u8, +} + +impl<'a> Unpack> for PackedTokenVaultSeeds { + type Unpacked = TokenVaultSeeds; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo<'a>], + ) -> std::result::Result { + let mint = *remaining_accounts + .get(self.mint_idx as usize) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)? + .key; + Ok(TokenVaultSeeds { mint }) + } +} diff --git a/sdk-tests/manual-test/src/token_account/mod.rs b/sdk-tests/anchor-manual-test/src/token_account/mod.rs similarity index 100% rename from sdk-tests/manual-test/src/token_account/mod.rs rename to sdk-tests/anchor-manual-test/src/token_account/mod.rs diff --git a/sdk-tests/manual-test/src/two_mints/accounts.rs b/sdk-tests/anchor-manual-test/src/two_mints/accounts.rs similarity index 97% rename from sdk-tests/manual-test/src/two_mints/accounts.rs rename to sdk-tests/anchor-manual-test/src/two_mints/accounts.rs index 1efbfeb388..dd0846d855 100644 --- a/sdk-tests/manual-test/src/two_mints/accounts.rs +++ b/sdk-tests/anchor-manual-test/src/two_mints/accounts.rs @@ -1,7 +1,7 @@ //! Standard Anchor accounts struct for create_derived_mints instruction. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; +use light_account::CreateAccountsProof; use solana_account_info::AccountInfo; /// Seed constants diff --git a/sdk-tests/anchor-manual-test/src/two_mints/derived.rs b/sdk-tests/anchor-manual-test/src/two_mints/derived.rs new file mode 100644 index 0000000000..d008dfcfa5 --- /dev/null +++ b/sdk-tests/anchor-manual-test/src/two_mints/derived.rs @@ -0,0 +1,131 @@ +//! Derived code - what the macro would generate. +//! Contains LightPreInit/LightFinalize trait implementations. + +use anchor_lang::prelude::*; +use light_account::{ + CpiAccounts, CpiAccountsConfig, CreateMints, CreateMintsStaticAccounts, LightFinalize, + LightPreInit, LightSdkTypesError, SingleMintParams, +}; +use solana_account_info::AccountInfo; + +use super::accounts::{ + CreateDerivedMintsAccounts, CreateDerivedMintsParams, MINT_SIGNER_0_SEED, MINT_SIGNER_1_SEED, +}; + +// ============================================================================ +// LightPreInit Implementation - Creates mints at START of instruction +// ============================================================================ + +impl<'info> LightPreInit, CreateDerivedMintsParams> + for CreateDerivedMintsAccounts<'info> +{ + fn light_pre_init( + &mut self, + remaining_accounts: &[AccountInfo<'info>], + params: &CreateDerivedMintsParams, + ) -> std::result::Result { + let inner = || -> std::result::Result { + // 1. Build CPI accounts + let system_accounts_offset = + params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + &self.payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + // Constants + const NUM_LIGHT_MINTS: usize = 2; + const NUM_LIGHT_PDAS: usize = 0; + #[allow(clippy::absurd_extreme_comparisons)] + const WITH_CPI_CONTEXT: bool = NUM_LIGHT_PDAS > 0 && NUM_LIGHT_MINTS > 0; + + // 2. Build mint params + let authority = self.authority.key(); + let mint_signer_0 = self.mint_signer_0.key(); + let mint_signer_1 = self.mint_signer_1.key(); + + let mint_signer_0_seeds: &[&[u8]] = &[ + MINT_SIGNER_0_SEED, + authority.as_ref(), + &[params.mint_signer_0_bump], + ]; + let mint_signer_1_seeds: &[&[u8]] = &[ + MINT_SIGNER_1_SEED, + authority.as_ref(), + &[params.mint_signer_1_bump], + ]; + + let sdk_mints: [SingleMintParams<'_>; NUM_LIGHT_MINTS] = [ + SingleMintParams { + decimals: 6, + mint_authority: authority.to_bytes(), + mint_bump: None, + freeze_authority: None, + mint_seed_pubkey: mint_signer_0.to_bytes(), + authority_seeds: None, + mint_signer_seeds: Some(mint_signer_0_seeds), + token_metadata: None, + }, + SingleMintParams { + decimals: 9, + mint_authority: authority.to_bytes(), + mint_bump: None, + freeze_authority: None, + mint_seed_pubkey: mint_signer_1.to_bytes(), + authority_seeds: None, + mint_signer_seeds: Some(mint_signer_1_seeds), + token_metadata: None, + }, + ]; + + // 3. Create mints + let payer_info = self.payer.to_account_info(); + let mint_seed_accounts = [ + self.mint_signer_0.to_account_info(), + self.mint_signer_1.to_account_info(), + ]; + let mint_accounts = [self.mint_0.to_account_info(), self.mint_1.to_account_info()]; + + CreateMints { + mints: &sdk_mints, + proof_data: ¶ms.create_accounts_proof, + mint_seed_accounts: &mint_seed_accounts, + mint_accounts: &mint_accounts, + static_accounts: CreateMintsStaticAccounts { + fee_payer: &payer_info, + compressible_config: &self.compressible_config, + rent_sponsor: &self.rent_sponsor, + cpi_authority: &self.cpi_authority, + }, + cpi_context_offset: NUM_LIGHT_PDAS as u8, + } + .invoke(&cpi_accounts)?; + + Ok(WITH_CPI_CONTEXT) + }; + inner() + } +} + +// ============================================================================ +// LightFinalize Implementation - No-op for mint-only flow +// ============================================================================ + +impl<'info> LightFinalize, CreateDerivedMintsParams> + for CreateDerivedMintsAccounts<'info> +{ + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo<'info>], + _params: &CreateDerivedMintsParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + // No-op for mint-only flow - create_mints already executed in light_pre_init + Ok(()) + } +} diff --git a/sdk-tests/manual-test/src/two_mints/mod.rs b/sdk-tests/anchor-manual-test/src/two_mints/mod.rs similarity index 100% rename from sdk-tests/manual-test/src/two_mints/mod.rs rename to sdk-tests/anchor-manual-test/src/two_mints/mod.rs diff --git a/sdk-tests/manual-test/tests/account_loader.rs b/sdk-tests/anchor-manual-test/tests/account_loader.rs similarity index 95% rename from sdk-tests/manual-test/tests/account_loader.rs rename to sdk-tests/anchor-manual-test/tests/account_loader.rs index bcb345011c..f98235b5a8 100644 --- a/sdk-tests/manual-test/tests/account_loader.rs +++ b/sdk-tests/anchor-manual-test/tests/account_loader.rs @@ -6,16 +6,16 @@ mod shared; use anchor_lang::{Discriminator, InstructionData, ToAccountMetas}; +use anchor_manual_test::{ + CreateZeroCopyParams, ZeroCopyRecord, ZeroCopyRecordSeeds, ZeroCopyRecordVariant, +}; +use light_account::IntoVariant; use light_client::interface::{ create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, CreateAccountsProofInput, PdaSpec, }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_program_test::{program_test::TestRpc, Indexer, Rpc}; -use light_sdk::interface::IntoVariant; -use manual_test::{ - CreateZeroCopyParams, ZeroCopyRecord, ZeroCopyRecordSeeds, ZeroCopyRecordVariant, -}; use solana_instruction::Instruction; use solana_keypair::Keypair; use solana_pubkey::Pubkey; @@ -24,7 +24,7 @@ use solana_signer::Signer; /// Test the full lifecycle for zero-copy accounts: create -> compress -> decompress. #[tokio::test] async fn test_zero_copy_create_compress_decompress() { - let program_id = manual_test::ID; + let program_id = anchor_manual_test::ID; let (mut rpc, payer, config_pda) = shared::setup_test_env().await; let owner = Keypair::new().pubkey(); @@ -46,14 +46,14 @@ async fn test_zero_copy_create_compress_decompress() { .await .unwrap(); - let accounts = manual_test::accounts::CreateZeroCopy { + let accounts = anchor_manual_test::accounts::CreateZeroCopy { fee_payer: payer.pubkey(), compression_config: config_pda, record: record_pda, system_program: solana_sdk::system_program::ID, }; - let instruction_data = manual_test::instruction::CreateZeroCopy { + let instruction_data = anchor_manual_test::instruction::CreateZeroCopy { params: CreateZeroCopyParams { create_accounts_proof: proof_result.create_accounts_proof, owner, @@ -185,7 +185,7 @@ async fn test_zero_copy_create_compress_decompress() { ); // state should be Decompressed after decompression - use light_sdk::compressible::CompressionState; + use light_account::CompressionState; assert_eq!( record.compression_info.state, CompressionState::Decompressed, diff --git a/sdk-tests/manual-test/tests/all.rs b/sdk-tests/anchor-manual-test/tests/all.rs similarity index 96% rename from sdk-tests/manual-test/tests/all.rs rename to sdk-tests/anchor-manual-test/tests/all.rs index 3f84251fd3..8ad3a53a20 100644 --- a/sdk-tests/manual-test/tests/all.rs +++ b/sdk-tests/anchor-manual-test/tests/all.rs @@ -10,6 +10,10 @@ mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; +use anchor_manual_test::{ + CreateAllParams, MinimalRecord, ZeroCopyRecord, ALL_BORSH_SEED, ALL_MINT_SIGNER_SEED, + ALL_TOKEN_VAULT_SEED, ALL_ZERO_COPY_SEED, +}; use borsh::BorshDeserialize; use light_client::interface::{get_create_accounts_proof, CreateAccountsProofInput}; use light_program_test::Rpc; @@ -20,10 +24,6 @@ use light_token::instruction::{ use light_token_interface::state::{ AccountState, BaseMint, Mint, MintMetadata, Token, ACCOUNT_TYPE_MINT, }; -use manual_test::{ - CreateAllParams, MinimalRecord, ZeroCopyRecord, ALL_BORSH_SEED, ALL_MINT_SIGNER_SEED, - ALL_TOKEN_VAULT_SEED, ALL_ZERO_COPY_SEED, -}; use solana_sdk::{ instruction::Instruction, pubkey::Pubkey, @@ -35,7 +35,7 @@ use solana_sdk::{ async fn test_create_all() { let (mut rpc, payer, config_pda_addr) = shared::setup_test_env().await; - let program_id = manual_test::ID; + let program_id = anchor_manual_test::ID; let authority = Keypair::new(); let owner = Keypair::new().pubkey(); let value: u64 = 42; @@ -86,7 +86,7 @@ async fn test_create_all() { value, }; - let accounts = manual_test::accounts::CreateAllAccounts { + let accounts = anchor_manual_test::accounts::CreateAllAccounts { payer: payer.pubkey(), authority: authority.pubkey(), compression_config: config_pda_addr, @@ -112,7 +112,7 @@ async fn test_create_all() { proof_result.remaining_accounts, ] .concat(), - data: manual_test::instruction::CreateAll { params }.data(), + data: anchor_manual_test::instruction::CreateAll { params }.data(), }; rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer, &authority]) diff --git a/sdk-tests/manual-test/tests/ata.rs b/sdk-tests/anchor-manual-test/tests/ata.rs similarity index 89% rename from sdk-tests/manual-test/tests/ata.rs rename to sdk-tests/anchor-manual-test/tests/ata.rs index 9ab79299b7..488470f783 100644 --- a/sdk-tests/manual-test/tests/ata.rs +++ b/sdk-tests/anchor-manual-test/tests/ata.rs @@ -3,13 +3,13 @@ mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; +use anchor_manual_test::CreateAtaParams; use borsh::BorshDeserialize; use light_program_test::Rpc; use light_token::instruction::{ config_pda, derive_associated_token_account, rent_sponsor_pda, LIGHT_TOKEN_PROGRAM_ID, }; use light_token_interface::state::{AccountState, Token}; -use manual_test::CreateAtaParams; use solana_sdk::{ instruction::Instruction, signature::{Keypair, Signer}, @@ -31,7 +31,7 @@ async fn test_create_ata() { let params = CreateAtaParams::default(); - let accounts = manual_test::accounts::CreateAtaAccounts { + let accounts = anchor_manual_test::accounts::CreateAtaAccounts { payer: payer.pubkey(), mint, ata_owner: ata_owner.pubkey(), @@ -43,9 +43,9 @@ async fn test_create_ata() { }; let ix = Instruction { - program_id: manual_test::ID, + program_id: anchor_manual_test::ID, accounts: accounts.to_account_metas(None), - data: manual_test::instruction::CreateAta { params }.data(), + data: anchor_manual_test::instruction::CreateAta { params }.data(), }; rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) @@ -79,7 +79,7 @@ async fn test_create_ata_idempotent() { let params = CreateAtaParams::default(); - let accounts = manual_test::accounts::CreateAtaAccounts { + let accounts = anchor_manual_test::accounts::CreateAtaAccounts { payer: payer.pubkey(), mint, ata_owner: ata_owner.pubkey(), @@ -91,9 +91,9 @@ async fn test_create_ata_idempotent() { }; let ix = Instruction { - program_id: manual_test::ID, + program_id: anchor_manual_test::ID, accounts: accounts.to_account_metas(None), - data: manual_test::instruction::CreateAta { + data: anchor_manual_test::instruction::CreateAta { params: params.clone(), } .data(), diff --git a/sdk-tests/manual-test/tests/shared.rs b/sdk-tests/anchor-manual-test/tests/shared.rs similarity index 85% rename from sdk-tests/manual-test/tests/shared.rs rename to sdk-tests/anchor-manual-test/tests/shared.rs index b4b6086f03..fc947c7ee5 100644 --- a/sdk-tests/manual-test/tests/shared.rs +++ b/sdk-tests/anchor-manual-test/tests/shared.rs @@ -1,6 +1,7 @@ //! Shared test helpers for manual-test integration tests. use anchor_lang::InstructionData; +use light_account::derive_rent_sponsor_pda; use light_client::interface::{ get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, }; @@ -8,7 +9,6 @@ use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest}, ProgramTestConfig, Rpc, }; -use light_sdk::utils::derive_rent_sponsor_pda; use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signer::Signer; @@ -16,8 +16,9 @@ use solana_signer::Signer; /// Setup test environment with Light Protocol and compression config. /// Returns (rpc, payer, config_pda). pub async fn setup_test_env() -> (LightProgramTest, Keypair, Pubkey) { - let program_id = manual_test::ID; - let mut config = ProgramTestConfig::new_v2(true, Some(vec![("manual_test", program_id)])); + let program_id = anchor_manual_test::ID; + let mut config = + ProgramTestConfig::new_v2(true, Some(vec![("anchor_manual_test", program_id)])); config = config.with_light_protocol_events(); let mut rpc = LightProgramTest::new(config).await.unwrap(); @@ -48,18 +49,18 @@ pub async fn setup_test_env() -> (LightProgramTest, Keypair, Pubkey) { #[allow(dead_code)] pub async fn create_test_mint(rpc: &mut LightProgramTest, payer: &Keypair) -> Pubkey { use anchor_lang::ToAccountMetas; - use manual_test::{CreateDerivedMintsParams, MINT_SIGNER_0_SEED, MINT_SIGNER_1_SEED}; + use anchor_manual_test::{CreateDerivedMintsParams, MINT_SIGNER_0_SEED, MINT_SIGNER_1_SEED}; let authority = Keypair::new(); // Derive mint signer PDAs let (mint_signer_0, mint_signer_0_bump) = Pubkey::find_program_address( &[MINT_SIGNER_0_SEED, authority.pubkey().as_ref()], - &manual_test::ID, + &anchor_manual_test::ID, ); let (mint_signer_1, mint_signer_1_bump) = Pubkey::find_program_address( &[MINT_SIGNER_1_SEED, authority.pubkey().as_ref()], - &manual_test::ID, + &anchor_manual_test::ID, ); // Derive mint PDAs @@ -69,7 +70,7 @@ pub async fn create_test_mint(rpc: &mut LightProgramTest, payer: &Keypair) -> Pu // Get proof for the mints let proof_result = get_create_accounts_proof( rpc, - &manual_test::ID, + &anchor_manual_test::ID, vec![ CreateAccountsProofInput::mint(mint_signer_0), CreateAccountsProofInput::mint(mint_signer_1), @@ -84,7 +85,7 @@ pub async fn create_test_mint(rpc: &mut LightProgramTest, payer: &Keypair) -> Pu mint_signer_1_bump, }; - let accounts = manual_test::accounts::CreateDerivedMintsAccounts { + let accounts = anchor_manual_test::accounts::CreateDerivedMintsAccounts { payer: payer.pubkey(), authority: authority.pubkey(), mint_signer_0, @@ -99,13 +100,13 @@ pub async fn create_test_mint(rpc: &mut LightProgramTest, payer: &Keypair) -> Pu }; let ix = solana_sdk::instruction::Instruction { - program_id: manual_test::ID, + program_id: anchor_manual_test::ID, accounts: [ accounts.to_account_metas(None), proof_result.remaining_accounts, ] .concat(), - data: manual_test::instruction::CreateDerivedMints { params }.data(), + data: anchor_manual_test::instruction::CreateDerivedMints { params }.data(), }; rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[payer, &authority]) diff --git a/sdk-tests/manual-test/tests/test.rs b/sdk-tests/anchor-manual-test/tests/test.rs similarity index 94% rename from sdk-tests/manual-test/tests/test.rs rename to sdk-tests/anchor-manual-test/tests/test.rs index 6bf6314a94..f6356bd0f3 100644 --- a/sdk-tests/manual-test/tests/test.rs +++ b/sdk-tests/anchor-manual-test/tests/test.rs @@ -5,17 +5,17 @@ mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; +use anchor_manual_test::{ + pda::{MinimalRecord, MinimalRecordSeeds, MinimalRecordVariant}, + CreatePdaParams, +}; +use light_account::IntoVariant; use light_client::interface::{ create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, CreateAccountsProofInput, PdaSpec, }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_program_test::{program_test::TestRpc, Indexer, Rpc}; -use light_sdk::interface::IntoVariant; -use manual_test::{ - pda::{MinimalRecord, MinimalRecordSeeds, MinimalRecordVariant}, - CreatePdaParams, -}; use solana_instruction::Instruction; use solana_keypair::Keypair; use solana_pubkey::Pubkey; @@ -24,7 +24,7 @@ use solana_signer::Signer; /// Test the full lifecycle: create -> compress -> decompress. #[tokio::test] async fn test_create_compress_decompress() { - let program_id = manual_test::ID; + let program_id = anchor_manual_test::ID; let (mut rpc, payer, config_pda) = shared::setup_test_env().await; let owner = Keypair::new().pubkey(); @@ -45,14 +45,14 @@ async fn test_create_compress_decompress() { .await .unwrap(); - let accounts = manual_test::accounts::CreatePda { + let accounts = anchor_manual_test::accounts::CreatePda { fee_payer: payer.pubkey(), compression_config: config_pda, record: record_pda, system_program: solana_sdk::system_program::ID, }; - let instruction_data = manual_test::instruction::CreatePda { + let instruction_data = anchor_manual_test::instruction::CreatePda { params: CreatePdaParams { create_accounts_proof: proof_result.create_accounts_proof, owner, @@ -157,7 +157,7 @@ async fn test_create_compress_decompress() { assert_eq!(record.owner, owner, "Record owner should match"); // state should be Decompressed after decompression - use light_sdk::compressible::CompressionState; + use light_account::CompressionState; assert_eq!( record.compression_info.state, CompressionState::Decompressed, diff --git a/sdk-tests/manual-test/tests/token_account.rs b/sdk-tests/anchor-manual-test/tests/token_account.rs similarity index 87% rename from sdk-tests/manual-test/tests/token_account.rs rename to sdk-tests/anchor-manual-test/tests/token_account.rs index 3b1ec64c99..a957536d54 100644 --- a/sdk-tests/manual-test/tests/token_account.rs +++ b/sdk-tests/anchor-manual-test/tests/token_account.rs @@ -3,11 +3,11 @@ mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; +use anchor_manual_test::{CreateTokenVaultParams, TOKEN_VAULT_SEED}; use borsh::BorshDeserialize; use light_program_test::Rpc; use light_token::instruction::{config_pda, rent_sponsor_pda, LIGHT_TOKEN_PROGRAM_ID}; use light_token_interface::state::{AccountState, Token}; -use manual_test::{CreateTokenVaultParams, TOKEN_VAULT_SEED}; use solana_sdk::{ instruction::Instruction, pubkey::Pubkey, @@ -27,11 +27,11 @@ async fn test_create_token_vault() { // Derive token vault PDA let (token_vault, vault_bump) = - Pubkey::find_program_address(&[TOKEN_VAULT_SEED, mint.as_ref()], &manual_test::ID); + Pubkey::find_program_address(&[TOKEN_VAULT_SEED, mint.as_ref()], &anchor_manual_test::ID); let params = CreateTokenVaultParams { vault_bump }; - let accounts = manual_test::accounts::CreateTokenVaultAccounts { + let accounts = anchor_manual_test::accounts::CreateTokenVaultAccounts { payer: payer.pubkey(), mint, vault_owner: vault_owner.pubkey(), @@ -43,9 +43,9 @@ async fn test_create_token_vault() { }; let ix = Instruction { - program_id: manual_test::ID, + program_id: anchor_manual_test::ID, accounts: accounts.to_account_metas(None), - data: manual_test::instruction::CreateTokenVault { params }.data(), + data: anchor_manual_test::instruction::CreateTokenVault { params }.data(), }; rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) diff --git a/sdk-tests/manual-test/tests/two_mints.rs b/sdk-tests/anchor-manual-test/tests/two_mints.rs similarity index 92% rename from sdk-tests/manual-test/tests/two_mints.rs rename to sdk-tests/anchor-manual-test/tests/two_mints.rs index 94757fd366..5e64a04c1e 100644 --- a/sdk-tests/manual-test/tests/two_mints.rs +++ b/sdk-tests/anchor-manual-test/tests/two_mints.rs @@ -3,6 +3,7 @@ mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; +use anchor_manual_test::{CreateDerivedMintsParams, MINT_SIGNER_0_SEED, MINT_SIGNER_1_SEED}; use borsh::BorshDeserialize; use light_client::interface::{get_create_accounts_proof, CreateAccountsProofInput}; use light_program_test::Rpc; @@ -10,7 +11,6 @@ use light_token::instruction::{ config_pda, find_mint_address, rent_sponsor_pda, LIGHT_TOKEN_PROGRAM_ID, }; use light_token_interface::state::{BaseMint, Mint, MintMetadata, ACCOUNT_TYPE_MINT}; -use manual_test::{CreateDerivedMintsParams, MINT_SIGNER_0_SEED, MINT_SIGNER_1_SEED}; use solana_sdk::{ instruction::Instruction, pubkey::Pubkey, @@ -27,11 +27,11 @@ async fn test_create_derived_mints() { // Derive mint signer PDAs from authority (like macro would) let (mint_signer_0, mint_signer_0_bump) = Pubkey::find_program_address( &[MINT_SIGNER_0_SEED, authority.pubkey().as_ref()], - &manual_test::ID, + &anchor_manual_test::ID, ); let (mint_signer_1, mint_signer_1_bump) = Pubkey::find_program_address( &[MINT_SIGNER_1_SEED, authority.pubkey().as_ref()], - &manual_test::ID, + &anchor_manual_test::ID, ); // Derive mint PDAs from mint signers (light-token derives these) @@ -41,7 +41,7 @@ async fn test_create_derived_mints() { // Get proof for the mints using the helper let proof_result = get_create_accounts_proof( &rpc, - &manual_test::ID, + &anchor_manual_test::ID, vec![ CreateAccountsProofInput::mint(mint_signer_0), CreateAccountsProofInput::mint(mint_signer_1), @@ -58,7 +58,7 @@ async fn test_create_derived_mints() { }; // Build accounts using Anchor's generated struct - let accounts = manual_test::accounts::CreateDerivedMintsAccounts { + let accounts = anchor_manual_test::accounts::CreateDerivedMintsAccounts { payer: payer.pubkey(), authority: authority.pubkey(), mint_signer_0, @@ -73,13 +73,13 @@ async fn test_create_derived_mints() { }; let ix = Instruction { - program_id: manual_test::ID, + program_id: anchor_manual_test::ID, accounts: [ accounts.to_account_metas(None), proof_result.remaining_accounts, ] .concat(), - data: manual_test::instruction::CreateDerivedMints { params }.data(), + data: anchor_manual_test::instruction::CreateDerivedMints { params }.data(), }; // Sign with payer and authority diff --git a/sdk-tests/anchor-semi-manual-test/Cargo.toml b/sdk-tests/anchor-semi-manual-test/Cargo.toml new file mode 100644 index 0000000000..5745ae853d --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "anchor-semi-manual-test" +version = "0.1.0" +description = "Test for #[derive(LightProgram)] macro validation with all variant kinds" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "anchor_semi_manual_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] +idl-build = ["anchor-lang/idl-build", "light-anchor-spl/idl-build"] +test-sbf = [] + +[dependencies] +light-account = { workspace = true, features = ["token", "anchor"] } +light-sdk-types = { workspace = true, features = ["anchor", "v2", "cpi-context"] } +light-macros = { workspace = true, features = ["solana"] } +bytemuck = { workspace = true, features = ["derive"] } +borsh = { workspace = true } +light-compressed-account = { workspace = true, features = ["solana"] } +anchor-lang = { workspace = true } +light-anchor-spl = { workspace = true, features = ["metadata"] } +light-compressible = { workspace = true, features = ["anchor"] } +light-hasher = { workspace = true, features = ["solana"] } +light-token-types = { workspace = true, features = ["anchor"] } +solana-program = { workspace = true } +solana-pubkey = { workspace = true } +solana-msg = { workspace = true } +solana-program-error = { workspace = true } +solana-account-info = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +light-client = { workspace = true, features = ["v2", "anchor"] } +light-test-utils = { workspace = true } +light-token = { workspace = true } +light-token-interface = { workspace = true } +tokio = { workspace = true } +solana-sdk = { workspace = true } +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-keypair = { workspace = true } +solana-signer = { workspace = true } +light-batched-merkle-tree = { workspace = true } +rand = { workspace = true } +solana-account = { workspace = true } + +[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/anchor-semi-manual-test/src/instruction_accounts.rs b/sdk-tests/anchor-semi-manual-test/src/instruction_accounts.rs new file mode 100644 index 0000000000..e4b63460b0 --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/src/instruction_accounts.rs @@ -0,0 +1,443 @@ +//! Accounts module for single-pda-derive-test. + +use anchor_lang::prelude::*; +use light_account::{LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_sdk_types::{interface::CreateAccountsProof, LIGHT_TOKEN_PROGRAM_ID}; + +use crate::{ + state::{MinimalRecord, ZeroCopyRecord}, + MINT_SIGNER_SEED_A, MINT_SIGNER_SEED_B, RECORD_SEED, VAULT_AUTH_SEED, VAULT_SEED, +}; + +// ============================================================================= +// 1. CreatePda +// ============================================================================= + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreatePdaParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Minimal accounts struct for testing single PDA creation. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreatePdaParams)] +pub struct CreatePda<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + /// CHECK: PDA rent sponsor for rent reimbursement + #[account(mut)] + pub pda_rent_sponsor: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + MinimalRecord::INIT_SPACE, + seeds = [b"minimal_record", params.owner.as_ref()], + bump, + )] + #[light_account(init)] + pub record: Account<'info, MinimalRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================= +// 2. CreateAta +// ============================================================================= + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateAtaParams { + pub create_accounts_proof: CreateAccountsProof, + pub ata_bump: u8, +} + +/// Accounts struct for testing single ATA creation. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateAtaParams)] +pub struct CreateAta<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Token mint for the ATA + pub ata_mint: AccountInfo<'info>, + + /// CHECK: Owner of the ATA + pub ata_owner: AccountInfo<'info>, + + /// ATA account - created via LightFinalize CPI. + #[account(mut)] + #[light_account(init, associated_token::authority = ata_owner, associated_token::mint = ata_mint, associated_token::bump = params.ata_bump)] + pub ata: UncheckedAccount<'info>, + + #[account(address = LIGHT_TOKEN_CONFIG)] + pub light_token_config: AccountInfo<'info>, + + #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] + pub light_token_rent_sponsor: AccountInfo<'info>, + + /// CHECK: Light Token Program for CPI + #[account(address = LIGHT_TOKEN_PROGRAM_ID.into())] + pub light_token_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================= +// 3. CreateTokenVault +// ============================================================================= + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateTokenVaultParams { + pub create_accounts_proof: CreateAccountsProof, + pub vault_bump: u8, +} + +/// Accounts struct for testing single token vault creation. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateTokenVaultParams)] +pub struct CreateTokenVault<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Token mint + pub mint: AccountInfo<'info>, + + #[account( + seeds = [VAULT_AUTH_SEED], + bump, + )] + pub vault_authority: UncheckedAccount<'info>, + + /// Token vault account - created via LightFinalize CPI. + #[account( + mut, + seeds = [VAULT_SEED, mint.key().as_ref()], + bump, + )] + #[light_account(init, token::seeds = [VAULT_SEED, self.mint.key()], token::mint = mint, token::owner = vault_authority, token::owner_seeds = [VAULT_AUTH_SEED])] + pub vault: UncheckedAccount<'info>, + + #[account(address = LIGHT_TOKEN_CONFIG)] + pub light_token_config: AccountInfo<'info>, + + #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] + pub light_token_rent_sponsor: AccountInfo<'info>, + + /// CHECK: Light token CPI authority + pub light_token_cpi_authority: AccountInfo<'info>, + + /// CHECK: Light token program for CPI + pub light_token_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================= +// 4. CreateZeroCopyRecord +// ============================================================================= + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateZeroCopyRecordParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Accounts struct for creating a zero-copy record. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateZeroCopyRecordParams)] +pub struct CreateZeroCopyRecord<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config PDA + pub compression_config: AccountInfo<'info>, + + /// CHECK: PDA rent sponsor for rent reimbursement + #[account(mut)] + pub pda_rent_sponsor: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + core::mem::size_of::(), + seeds = [RECORD_SEED, params.owner.as_ref()], + bump, + )] + #[light_account(init, zero_copy)] + pub record: AccountLoader<'info, ZeroCopyRecord>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================= +// 5. CreateMint +// ============================================================================= + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateMintParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_bump: u8, +} + +/// Accounts struct for testing single mint creation. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateMintParams)] +pub struct CreateMint<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + /// CHECK: PDA derived from authority + #[account( + seeds = [MINT_SIGNER_SEED_A, authority.key().as_ref()], + bump, + )] + pub mint_signer: UncheckedAccount<'info>, + + /// CHECK: Initialized by light_mint CPI + #[account(mut)] + #[light_account(init, + mint::signer = mint_signer, + mint::authority = fee_payer, + mint::decimals = 9, + mint::seeds = &[MINT_SIGNER_SEED_A, self.authority.to_account_info().key.as_ref()], + mint::bump = params.mint_signer_bump + )] + pub mint: UncheckedAccount<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + /// CHECK: CToken config + #[account(address = LIGHT_TOKEN_CONFIG)] + pub light_token_config: AccountInfo<'info>, + + /// CHECK: CToken rent sponsor + #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] + pub light_token_rent_sponsor: AccountInfo<'info>, + + /// CHECK: CToken program + pub light_token_program: AccountInfo<'info>, + + /// CHECK: CToken CPI authority + pub light_token_cpi_authority: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================= +// 6. CreateTwoMints +// ============================================================================= + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateTwoMintsParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_bump_a: u8, + pub mint_signer_bump_b: u8, +} + +/// Accounts struct for testing two mint creation in a single instruction. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateTwoMintsParams)] +pub struct CreateTwoMints<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + /// CHECK: PDA for mint A + #[account( + seeds = [MINT_SIGNER_SEED_A, authority.key().as_ref()], + bump, + )] + pub mint_signer_a: UncheckedAccount<'info>, + + /// CHECK: Mint A - initialized by light_mint CPI + #[account(mut)] + #[light_account(init, + mint::signer = mint_signer_a, + mint::authority = fee_payer, + mint::decimals = 9, + mint::seeds = &[MINT_SIGNER_SEED_A, self.authority.to_account_info().key.as_ref()], + mint::bump = params.mint_signer_bump_a + )] + pub mint_a: UncheckedAccount<'info>, + + /// CHECK: PDA for mint B + #[account( + seeds = [MINT_SIGNER_SEED_B, authority.key().as_ref()], + bump, + )] + pub mint_signer_b: UncheckedAccount<'info>, + + /// CHECK: Mint B - initialized by light_mint CPI + #[account(mut)] + #[light_account(init, + mint::signer = mint_signer_b, + mint::authority = fee_payer, + mint::decimals = 6, + mint::seeds = &[MINT_SIGNER_SEED_B, self.authority.to_account_info().key.as_ref()], + mint::bump = params.mint_signer_bump_b + )] + pub mint_b: UncheckedAccount<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + /// CHECK: CToken config + #[account(address = LIGHT_TOKEN_CONFIG)] + pub light_token_config: AccountInfo<'info>, + + /// CHECK: CToken rent sponsor + #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] + pub light_token_rent_sponsor: AccountInfo<'info>, + + /// CHECK: CToken program + pub light_token_program: AccountInfo<'info>, + + /// CHECK: CToken CPI authority + pub light_token_cpi_authority: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +// ============================================================================= +// 7. CreateAll +// ============================================================================= + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateAllParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, + pub ata_bump: u8, + pub vault_bump: u8, + pub mint_signer_bump_a: u8, + pub mint_signer_bump_b: u8, +} + +/// Combined accounts struct exercising all variant types in one instruction. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateAllParams)] +pub struct CreateAll<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + // -- PDA -- + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + /// CHECK: PDA rent sponsor + #[account(mut)] + pub pda_rent_sponsor: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + MinimalRecord::INIT_SPACE, + seeds = [b"minimal_record", params.owner.as_ref()], + bump, + )] + #[light_account(init)] + pub record: Account<'info, MinimalRecord>, + + // -- Zero-copy -- + #[account( + init, + payer = fee_payer, + space = 8 + core::mem::size_of::(), + seeds = [RECORD_SEED, params.owner.as_ref()], + bump, + )] + #[light_account(init, zero_copy)] + pub zero_copy_record: AccountLoader<'info, ZeroCopyRecord>, + + // -- ATA -- + /// CHECK: Token mint for the ATA + pub ata_mint: AccountInfo<'info>, + + /// CHECK: Owner of the ATA + pub ata_owner: AccountInfo<'info>, + + #[account(mut)] + #[light_account(init, associated_token::authority = ata_owner, associated_token::mint = ata_mint, associated_token::bump = params.ata_bump)] + pub ata: UncheckedAccount<'info>, + + // -- Token vault -- + /// CHECK: Token mint for the vault + pub vault_mint: AccountInfo<'info>, + + #[account( + seeds = [VAULT_AUTH_SEED], + bump, + )] + pub vault_authority: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [VAULT_SEED, vault_mint.key().as_ref()], + bump, + )] + #[light_account(init, token::seeds = [VAULT_SEED, self.vault_mint.key()], token::mint = vault_mint, token::owner = vault_authority, token::owner_seeds = [VAULT_AUTH_SEED])] + pub vault: UncheckedAccount<'info>, + + // -- Mint A -- + pub authority: Signer<'info>, + + /// CHECK: PDA for mint A + #[account( + seeds = [MINT_SIGNER_SEED_A, authority.key().as_ref()], + bump, + )] + pub mint_signer_a: UncheckedAccount<'info>, + + #[account(mut)] + #[light_account(init, + mint::signer = mint_signer_a, + mint::authority = fee_payer, + mint::decimals = 9, + mint::seeds = &[MINT_SIGNER_SEED_A, self.authority.to_account_info().key.as_ref()], + mint::bump = params.mint_signer_bump_a + )] + pub mint_a: UncheckedAccount<'info>, + + // -- Mint B -- + /// CHECK: PDA for mint B + #[account( + seeds = [MINT_SIGNER_SEED_B, authority.key().as_ref()], + bump, + )] + pub mint_signer_b: UncheckedAccount<'info>, + + #[account(mut)] + #[light_account(init, + mint::signer = mint_signer_b, + mint::authority = fee_payer, + mint::decimals = 6, + mint::seeds = &[MINT_SIGNER_SEED_B, self.authority.to_account_info().key.as_ref()], + mint::bump = params.mint_signer_bump_b + )] + pub mint_b: UncheckedAccount<'info>, + + // -- Infrastructure -- + /// CHECK: CToken config + #[account(address = LIGHT_TOKEN_CONFIG)] + pub light_token_config: AccountInfo<'info>, + + /// CHECK: CToken rent sponsor + #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] + pub light_token_rent_sponsor: AccountInfo<'info>, + + /// CHECK: CToken CPI authority + pub light_token_cpi_authority: AccountInfo<'info>, + + /// CHECK: CToken program + pub light_token_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/anchor-semi-manual-test/src/lib.rs b/sdk-tests/anchor-semi-manual-test/src/lib.rs new file mode 100644 index 0000000000..154c7974fe --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/src/lib.rs @@ -0,0 +1,330 @@ +//! Test program for #[derive(LightProgram)] macro validation. +//! +//! Uses #[derive(LightProgram)] with plain #[program] (no #[light_program]). +//! Exercises all variant kinds: PDA, ATA, token, zero_copy. + +#![allow(deprecated)] + +use std::marker::PhantomData; + +use anchor_lang::prelude::*; +use light_account::{derive_light_cpi_signer, CpiSigner, LightProgram}; + +pub mod instruction_accounts; +pub mod state; + +pub use instruction_accounts::*; +pub use state::*; + +declare_id!("DrvPda11111111111111111111111111111111111111"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("DrvPda11111111111111111111111111111111111111"); + +pub const VAULT_AUTH_SEED: &[u8] = b"vault_auth"; +pub const VAULT_SEED: &[u8] = b"vault"; +pub const RECORD_SEED: &[u8] = b"zero_copy_record"; +pub const MINT_SIGNER_SEED_A: &[u8] = b"mint_signer_a"; +pub const MINT_SIGNER_SEED_B: &[u8] = b"mint_signer_b"; + +#[derive(LightProgram)] +pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"minimal_record", ctx.owner])] + MinimalRecord(MinimalRecord), + + #[light_account(associated_token)] + Ata, + + #[light_account(token::seeds = [VAULT_SEED, ctx.mint], token::owner_seeds = [VAULT_AUTH_SEED])] + Vault, + + #[light_account(pda::seeds = [RECORD_SEED, ctx.owner], pda::zero_copy)] + ZeroCopyRecord(ZeroCopyRecord), +} + +#[program] +pub mod anchor_semi_manual_test { + use light_account::{light_err, LightFinalize, LightPreInit}; + + use super::*; + + // ========================================================================= + // Compression infrastructure (generated by #[derive(LightProgram)]) + // ========================================================================= + + pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, EmptyAccounts<'info>>, + params: light_account::CompressAndCloseParams, + ) -> Result<()> { + light_account::process_compress_pda_accounts_idempotent( + ctx.remaining_accounts, + ¶ms, + ProgramAccounts::compress_dispatch, + LIGHT_CPI_SIGNER, + &LIGHT_CPI_SIGNER.program_id, + ) + .map_err(Into::into) + } + + pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, EmptyAccounts<'info>>, + params: light_account::DecompressIdempotentParams, + ) -> Result<()> { + use solana_program::{clock::Clock, sysvar::Sysvar}; + let current_slot = Clock::get()?.slot; + ProgramAccounts::decompress_dispatch( + ctx.remaining_accounts, + ¶ms, + LIGHT_CPI_SIGNER, + &LIGHT_CPI_SIGNER.program_id, + current_slot, + ) + .map_err(Into::into) + } + + pub fn initialize_compression_config<'info>( + ctx: Context<'_, '_, '_, 'info, EmptyAccounts<'info>>, + params: InitConfigParams, + ) -> Result<()> { + light_account::process_initialize_light_config( + &ctx.remaining_accounts[1], // config + &ctx.remaining_accounts[3], // authority + ¶ms.rent_sponsor.to_bytes(), + ¶ms.compression_authority.to_bytes(), + params.rent_config, + params.write_top_up, + params.address_space.iter().map(|p| p.to_bytes()).collect(), + 0, // config_bump + &ctx.remaining_accounts[0], // payer + &ctx.remaining_accounts[4], // system_program + &LIGHT_CPI_SIGNER.program_id, + ) + .map_err(Into::into) + } + + pub fn update_compression_config<'info>( + ctx: Context<'_, '_, '_, 'info, EmptyAccounts<'info>>, + instruction_data: Vec, + ) -> Result<()> { + light_account::process_update_light_config( + ctx.remaining_accounts, + &instruction_data, + &LIGHT_CPI_SIGNER.program_id, + ) + .map_err(Into::into) + } + + // ========================================================================= + // Instruction handlers (manually written for #[derive(LightProgram)]) + // ========================================================================= + + pub fn create_pda<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePda<'info>>, + params: CreatePdaParams, + ) -> Result<()> { + let has_pre_init = ctx + .accounts + .light_pre_init(ctx.remaining_accounts, ¶ms) + .map_err(light_err)?; + ctx.accounts.record.owner = params.owner; + ctx.accounts + .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) + .map_err(light_err)?; + Ok(()) + } + + pub fn create_ata<'info>( + ctx: Context<'_, '_, '_, 'info, CreateAta<'info>>, + params: CreateAtaParams, + ) -> Result<()> { + let has_pre_init = ctx + .accounts + .light_pre_init(ctx.remaining_accounts, ¶ms) + .map_err(light_err)?; + ctx.accounts + .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) + .map_err(light_err)?; + Ok(()) + } + + pub fn create_token_vault<'info>( + ctx: Context<'_, '_, '_, 'info, CreateTokenVault<'info>>, + params: CreateTokenVaultParams, + ) -> Result<()> { + let has_pre_init = ctx + .accounts + .light_pre_init(ctx.remaining_accounts, ¶ms) + .map_err(light_err)?; + ctx.accounts + .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) + .map_err(light_err)?; + Ok(()) + } + + pub fn create_zero_copy_record<'info>( + ctx: Context<'_, '_, '_, 'info, CreateZeroCopyRecord<'info>>, + params: CreateZeroCopyRecordParams, + ) -> Result<()> { + let has_pre_init = ctx + .accounts + .light_pre_init(ctx.remaining_accounts, ¶ms) + .map_err(light_err)?; + { + let mut record = ctx.accounts.record.load_init()?; + record.owner = params.owner; + } + ctx.accounts + .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) + .map_err(light_err)?; + Ok(()) + } + + pub fn create_mint<'info>( + ctx: Context<'_, '_, '_, 'info, CreateMint<'info>>, + params: CreateMintParams, + ) -> Result<()> { + let has_pre_init = ctx + .accounts + .light_pre_init(ctx.remaining_accounts, ¶ms) + .map_err(light_err)?; + ctx.accounts + .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) + .map_err(light_err)?; + Ok(()) + } + + pub fn create_two_mints<'info>( + ctx: Context<'_, '_, '_, 'info, CreateTwoMints<'info>>, + params: CreateTwoMintsParams, + ) -> Result<()> { + let has_pre_init = ctx + .accounts + .light_pre_init(ctx.remaining_accounts, ¶ms) + .map_err(light_err)?; + ctx.accounts + .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) + .map_err(light_err)?; + Ok(()) + } + + pub fn create_all<'info>( + ctx: Context<'_, '_, '_, 'info, CreateAll<'info>>, + params: CreateAllParams, + ) -> Result<()> { + let has_pre_init = ctx + .accounts + .light_pre_init(ctx.remaining_accounts, ¶ms) + .map_err(light_err)?; + ctx.accounts.record.owner = params.owner; + { + let mut record = ctx.accounts.zero_copy_record.load_init()?; + record.owner = params.owner; + } + ctx.accounts + .light_finalize(ctx.remaining_accounts, ¶ms, has_pre_init) + .map_err(light_err)?; + Ok(()) + } +} + +/// Accounts struct for compress instruction. +/// Uses PhantomData for the `<'info>` lifetime so Anchor's CPI codegen works. +/// All accounts are passed via remaining_accounts. +pub struct EmptyAccounts<'info>(PhantomData<&'info ()>); + +impl<'info> anchor_lang::Accounts<'info, EmptyAccountsBumps> for EmptyAccounts<'info> { + fn try_accounts( + _program_id: &anchor_lang::solana_program::pubkey::Pubkey, + _accounts: &mut &'info [anchor_lang::solana_program::account_info::AccountInfo<'info>], + _ix_data: &[u8], + _bumps: &mut EmptyAccountsBumps, + _reallocs: &mut std::collections::BTreeSet, + ) -> anchor_lang::Result { + Ok(EmptyAccounts(PhantomData)) + } +} + +#[derive(Debug, Default)] +pub struct EmptyAccountsBumps {} + +impl<'info> anchor_lang::Bumps for EmptyAccounts<'info> { + type Bumps = EmptyAccountsBumps; +} + +impl<'info> anchor_lang::ToAccountInfos<'info> for EmptyAccounts<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + Vec::new() + } +} + +impl<'info> anchor_lang::ToAccountMetas for EmptyAccounts<'info> { + fn to_account_metas( + &self, + _is_signer: Option, + ) -> Vec { + Vec::new() + } +} + +impl<'info> anchor_lang::AccountsExit<'info> for EmptyAccounts<'info> { + fn exit( + &self, + _program_id: &anchor_lang::solana_program::pubkey::Pubkey, + ) -> anchor_lang::Result<()> { + Ok(()) + } +} + +#[cfg(feature = "idl-build")] +impl<'info> EmptyAccounts<'info> { + pub fn __anchor_private_gen_idl_accounts( + _accounts: &mut std::collections::BTreeMap, + _types: &mut std::collections::BTreeMap, + ) -> Vec { + Vec::new() + } +} + +pub(crate) mod __client_accounts_empty_accounts { + use super::*; + pub struct EmptyAccounts<'info>(PhantomData<&'info ()>); + impl<'info> borsh::ser::BorshSerialize for EmptyAccounts<'info> { + fn serialize( + &self, + _writer: &mut W, + ) -> ::core::result::Result<(), borsh::maybestd::io::Error> { + Ok(()) + } + } + impl<'info> anchor_lang::ToAccountMetas for EmptyAccounts<'info> { + fn to_account_metas( + &self, + _is_signer: Option, + ) -> Vec { + Vec::new() + } + } +} + +pub(crate) mod __cpi_client_accounts_empty_accounts { + use super::*; + #[allow(dead_code)] + pub struct EmptyAccounts<'info>(PhantomData<&'info ()>); + impl<'info> anchor_lang::ToAccountMetas for EmptyAccounts<'info> { + fn to_account_metas( + &self, + _is_signer: Option, + ) -> Vec { + Vec::new() + } + } + impl<'info> anchor_lang::ToAccountInfos<'info> for EmptyAccounts<'info> { + fn to_account_infos( + &self, + ) -> Vec> { + Vec::new() + } + } +} diff --git a/sdk-tests/anchor-semi-manual-test/src/state.rs b/sdk-tests/anchor-semi-manual-test/src/state.rs new file mode 100644 index 0000000000..524bd64bdd --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/src/state.rs @@ -0,0 +1,24 @@ +//! State module for single-pda-derive-test. + +use anchor_lang::prelude::*; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; + +/// Minimal record struct for testing PDA creation. +/// Contains only compression_info and one field. +#[derive(Default, Debug, PartialEq, InitSpace, LightAccount)] +#[account] +pub struct MinimalRecord { + pub compression_info: CompressionInfo, + pub owner: Pubkey, +} + +/// A zero-copy account using Pod serialization. +/// Used with AccountLoader for efficient on-chain zero-copy access. +#[derive(Default, Debug, PartialEq, LightAccount)] +#[account(zero_copy)] +#[repr(C)] +pub struct ZeroCopyRecord { + pub compression_info: CompressionInfo, + pub owner: Pubkey, + pub counter: u64, +} diff --git a/sdk-tests/anchor-semi-manual-test/tests/shared/mod.rs b/sdk-tests/anchor-semi-manual-test/tests/shared/mod.rs new file mode 100644 index 0000000000..293c26399c --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/tests/shared/mod.rs @@ -0,0 +1,218 @@ +#![allow(dead_code)] + +use light_account::derive_rent_sponsor_pda; +use light_client::interface::InitializeRentFreeConfig; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest}, + Indexer, ProgramTestConfig, Rpc, +}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Shared test environment with initialized compression config. +pub struct TestEnv { + pub rpc: LightProgramTest, + pub payer: Keypair, + pub program_id: Pubkey, + pub config_pda: Pubkey, + pub rent_sponsor: Pubkey, +} + +/// Sets up a test environment with program, config, and rent sponsor initialized. +pub async fn setup_test_env() -> TestEnv { + let program_id = anchor_semi_manual_test::ID; + let mut config = + ProgramTestConfig::new_v2(true, Some(vec![("anchor_semi_manual_test", program_id)])); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let (rent_sponsor, _) = derive_rent_sponsor_pda(&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"); + + TestEnv { + rpc, + payer, + program_id, + config_pda, + rent_sponsor, + } +} + +/// Creates a compressed mint using the ctoken SDK. +/// Returns (mint_pda, mint_seed_keypair). +pub async fn setup_create_mint( + rpc: &mut (impl Rpc + Indexer), + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, +) -> (Pubkey, Keypair) { + use light_token::instruction::{CreateMint, CreateMintParams}; + + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_seed.pubkey(), + &address_tree.tree, + ); + + let (mint, bump) = light_token::instruction::find_mint_address(&mint_seed.pubkey()); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + (mint, mint_seed) +} + +pub async fn assert_onchain_exists(rpc: &mut LightProgramTest, pda: &Pubkey, name: &str) { + assert!( + rpc.get_account(*pda).await.unwrap().is_some(), + "{} account ({}) should exist on-chain", + name, + pda + ); +} + +pub async fn assert_onchain_closed(rpc: &mut LightProgramTest, pda: &Pubkey, name: &str) { + let acc = rpc.get_account(*pda).await.unwrap(); + assert!( + acc.is_none() || acc.unwrap().lamports == 0, + "{} account ({}) should be closed", + name, + pda + ); +} + +pub async fn assert_compressed_token_exists( + rpc: &mut LightProgramTest, + owner: &Pubkey, + expected_amount: u64, + name: &str, +) { + let accs = rpc + .get_compressed_token_accounts_by_owner(owner, None, None) + .await + .unwrap() + .value + .items; + assert!( + !accs.is_empty(), + "{} compressed token account should exist for owner {}", + name, + owner + ); + assert_eq!( + accs[0].token.amount, expected_amount, + "{} token amount mismatch", + name + ); +} + +pub async fn assert_rent_sponsor_paid_for_accounts( + rpc: &mut LightProgramTest, + rent_sponsor: &Pubkey, + rent_sponsor_balance_before: u64, + created_accounts: &[Pubkey], +) { + let rent_sponsor_balance_after = rpc + .get_account(*rent_sponsor) + .await + .expect("get rent sponsor account") + .map(|a| a.lamports) + .unwrap_or(0); + + let mut total_account_lamports = 0u64; + for account in created_accounts { + let account_lamports = rpc + .get_account(*account) + .await + .expect("get created account") + .map(|a| a.lamports) + .unwrap_or(0); + total_account_lamports += account_lamports; + } + + let rent_sponsor_paid = rent_sponsor_balance_before.saturating_sub(rent_sponsor_balance_after); + + assert!( + rent_sponsor_paid >= total_account_lamports, + "Rent sponsor should have paid at least {} lamports for accounts, but only paid {}. \ + Before: {}, After: {}", + total_account_lamports, + rent_sponsor_paid, + rent_sponsor_balance_before, + rent_sponsor_balance_after + ); +} + +pub fn expected_compression_info( + actual: &light_account::CompressionInfo, +) -> light_account::CompressionInfo { + light_account::CompressionInfo { + last_claimed_slot: actual.last_claimed_slot, + lamports_per_write: 5000, + config_version: 1, + state: actual.state, + _padding: 0, + rent_config: light_compressible::rent::RentConfig::default(), + } +} + +pub fn parse_token(data: &[u8]) -> light_token_interface::state::token::Token { + borsh::BorshDeserialize::deserialize(&mut &data[..]).unwrap() +} diff --git a/sdk-tests/anchor-semi-manual-test/tests/stress_test.rs b/sdk-tests/anchor-semi-manual-test/tests/stress_test.rs new file mode 100644 index 0000000000..4d3d2972a0 --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/tests/stress_test.rs @@ -0,0 +1,527 @@ +/// Stress test: 20-iteration compression/decompression cycles for all account types. +/// +/// Tests repeated cycles of: +/// 1. Decompress all accounts +/// 2. Assert cached state matches on-chain state +/// 3. Update cache from on-chain state +/// 4. Compress all accounts (warp forward) +mod shared; + +use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; +use anchor_semi_manual_test::{ + CreateAllParams, LightAccountVariant, MinimalRecord, MinimalRecordSeeds, VaultSeeds, + ZeroCopyRecord, ZeroCopyRecordSeeds, MINT_SIGNER_SEED_A, MINT_SIGNER_SEED_B, RECORD_SEED, + VAULT_AUTH_SEED, VAULT_SEED, +}; +use light_batched_merkle_tree::{ + initialize_address_tree::InitAddressTreeAccountsInstructionData, + initialize_state_tree::InitStateTreeAccountsInstructionData, +}; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountInterfaceExt, + AccountSpec, ColdContext, CreateAccountsProofInput, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, + ProgramTestConfig, Rpc, +}; +use light_token_interface::state::{token::Token, Mint}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Stores all derived PDAs +#[allow(dead_code)] +struct TestPdas { + record: Pubkey, + zc_record: Pubkey, + ata: Pubkey, + ata_owner: Pubkey, + ata_mint: Pubkey, + vault: Pubkey, + vault_authority: Pubkey, + vault_mint: Pubkey, + mint_a: Pubkey, + mint_b: Pubkey, +} + +/// Cached state for accounts that go through the compress/decompress cycle. +#[derive(Clone)] +struct CachedState { + record: MinimalRecord, + zc_record: ZeroCopyRecord, + ata_token: Token, + vault_token: Token, + owner: Pubkey, +} + +/// Test context +struct StressTestContext { + rpc: LightProgramTest, + payer: Keypair, + config_pda: Pubkey, + program_id: Pubkey, +} + +fn parse_token(data: &[u8]) -> Token { + borsh::BorshDeserialize::deserialize(&mut &data[..]).unwrap() +} + +/// Setup environment with larger queues for stress test +async fn setup() -> (StressTestContext, TestPdas) { + let program_id = anchor_semi_manual_test::ID; + let mut config = + ProgramTestConfig::new_v2(true, Some(vec![("anchor_semi_manual_test", program_id)])) + .with_light_protocol_events(); + config.v2_state_tree_config = Some(InitStateTreeAccountsInstructionData::e2e_test_default()); + config.v2_address_tree_config = + Some(InitAddressTreeAccountsInstructionData::e2e_test_default()); + + 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 (rent_sponsor, _) = light_account::derive_rent_sponsor_pda(&program_id); + + let (init_config_ix, config_pda) = light_client::interface::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"); + + // Setup pre-existing mints for ATA and vault + let (ata_mint, _) = shared::setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9).await; + let (vault_mint, _) = shared::setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9).await; + + let owner = Keypair::new().pubkey(); + let authority = Keypair::new(); + + // Derive all PDAs + let (record_pda, _) = + Pubkey::find_program_address(&[b"minimal_record", owner.as_ref()], &program_id); + let (zc_record_pda, _) = + Pubkey::find_program_address(&[RECORD_SEED, owner.as_ref()], &program_id); + let ata_owner = payer.pubkey(); + let (ata, ata_bump) = light_token::instruction::derive_token_ata(&ata_owner, &ata_mint); + let (vault_authority, _) = Pubkey::find_program_address(&[VAULT_AUTH_SEED], &program_id); + let (vault, vault_bump) = + Pubkey::find_program_address(&[VAULT_SEED, vault_mint.as_ref()], &program_id); + let (mint_signer_a, mint_signer_bump_a) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_A, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_a_pda, _) = light_token::instruction::find_mint_address(&mint_signer_a); + let (mint_signer_b, mint_signer_bump_b) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_B, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_b_pda, _) = light_token::instruction::find_mint_address(&mint_signer_b); + + // Create all accounts in one instruction + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::pda(record_pda), + CreateAccountsProofInput::pda(zc_record_pda), + CreateAccountsProofInput::mint(mint_signer_a), + CreateAccountsProofInput::mint(mint_signer_b), + ], + ) + .await + .unwrap(); + + let accounts = anchor_semi_manual_test::accounts::CreateAll { + fee_payer: payer.pubkey(), + compression_config: config_pda, + pda_rent_sponsor: rent_sponsor, + record: record_pda, + zero_copy_record: zc_record_pda, + ata_mint, + ata_owner, + ata, + vault_mint, + vault_authority, + vault, + authority: authority.pubkey(), + mint_signer_a, + mint_a: mint_a_pda, + mint_signer_b, + mint_b: mint_b_pda, + light_token_config: light_token::instruction::LIGHT_TOKEN_CONFIG, + light_token_rent_sponsor: light_token::instruction::LIGHT_TOKEN_RENT_SPONSOR, + light_token_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + light_token_program: light_sdk_types::LIGHT_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = anchor_semi_manual_test::instruction::CreateAll { + params: CreateAllParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + ata_bump, + vault_bump, + mint_signer_bump_a, + mint_signer_bump_b, + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateAll should succeed"); + + let pdas = TestPdas { + record: record_pda, + zc_record: zc_record_pda, + ata, + ata_owner, + ata_mint, + vault, + vault_authority, + vault_mint, + mint_a: mint_a_pda, + mint_b: mint_b_pda, + }; + + let ctx = StressTestContext { + rpc, + payer, + config_pda, + program_id, + }; + + (ctx, pdas) +} + +/// Re-read all on-chain accounts into the cache +async fn refresh_cache(rpc: &mut LightProgramTest, pdas: &TestPdas, owner: Pubkey) -> CachedState { + let record_account = rpc.get_account(pdas.record).await.unwrap().unwrap(); + let record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &record_account.data[8..]).unwrap(); + + let zc_account = rpc.get_account(pdas.zc_record).await.unwrap().unwrap(); + let zc_record: ZeroCopyRecord = *bytemuck::from_bytes(&zc_account.data[8..]); + + let ata_token = parse_token(&rpc.get_account(pdas.ata).await.unwrap().unwrap().data); + let vault_token = parse_token(&rpc.get_account(pdas.vault).await.unwrap().unwrap().data); + + CachedState { + record, + zc_record, + ata_token, + vault_token, + owner, + } +} + +/// Decompress all accounts +async fn decompress_all(ctx: &mut StressTestContext, pdas: &TestPdas, cached: &CachedState) { + // PDA: MinimalRecord + let record_interface = ctx + .rpc + .get_account_interface(&pdas.record, &ctx.program_id) + .await + .expect("failed to get MinimalRecord interface"); + assert!(record_interface.is_cold(), "MinimalRecord should be cold"); + + let record_data = MinimalRecord::deserialize(&mut &record_interface.account.data[8..]) + .expect("Failed to parse MinimalRecord"); + let record_variant = LightAccountVariant::MinimalRecord { + seeds: MinimalRecordSeeds { + owner: cached.owner, + }, + data: record_data, + }; + let record_spec = PdaSpec::new(record_interface, record_variant, ctx.program_id); + + // PDA: ZeroCopyRecord + let zc_interface = ctx + .rpc + .get_account_interface(&pdas.zc_record, &ctx.program_id) + .await + .expect("failed to get ZeroCopyRecord interface"); + assert!(zc_interface.is_cold(), "ZeroCopyRecord should be cold"); + + let zc_data = ZeroCopyRecord::deserialize(&mut &zc_interface.account.data[8..]) + .expect("Failed to parse ZeroCopyRecord"); + let zc_variant = LightAccountVariant::ZeroCopyRecord { + seeds: ZeroCopyRecordSeeds { + owner: cached.owner, + }, + data: zc_data, + }; + let zc_spec = PdaSpec::new(zc_interface, zc_variant, ctx.program_id); + + // ATA + let ata_interface = ctx + .rpc + .get_ata_interface(&pdas.ata_owner, &pdas.ata_mint) + .await + .expect("failed to get ATA interface"); + assert!(ata_interface.is_cold(), "ATA should be cold"); + + // Token PDA: Vault + let vault_iface = ctx + .rpc + .get_token_account_interface(&pdas.vault) + .await + .expect("failed to get vault interface"); + assert!(vault_iface.is_cold(), "Vault should be cold"); + + let vault_token_data: Token = + borsh::BorshDeserialize::deserialize(&mut &vault_iface.account.data[..]) + .expect("Failed to parse vault Token"); + let vault_variant = LightAccountVariant::Vault(light_account::token::TokenDataWithSeeds { + seeds: VaultSeeds { + mint: pdas.vault_mint, + }, + token_data: vault_token_data, + }); + let vault_compressed = vault_iface + .compressed() + .expect("cold vault must have compressed data"); + let vault_interface = AccountInterface { + key: vault_iface.key, + account: vault_iface.account.clone(), + cold: Some(ColdContext::Account(vault_compressed.account.clone())), + }; + let vault_spec = PdaSpec::new(vault_interface, vault_variant, ctx.program_id); + + // Mint A + let mint_a_iface = ctx + .rpc + .get_mint_interface(&pdas.mint_a) + .await + .expect("failed to get mint A interface"); + assert!(mint_a_iface.is_cold(), "Mint A should be cold"); + let (compressed_a, _) = mint_a_iface + .compressed() + .expect("cold mint A must have compressed data"); + let mint_a_ai = AccountInterface { + key: pdas.mint_a, + account: solana_account::Account { + lamports: 0, + data: vec![], + owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Account(compressed_a.clone())), + }; + + // Mint B + let mint_b_iface = ctx + .rpc + .get_mint_interface(&pdas.mint_b) + .await + .expect("failed to get mint B interface"); + assert!(mint_b_iface.is_cold(), "Mint B should be cold"); + let (compressed_b, _) = mint_b_iface + .compressed() + .expect("cold mint B must have compressed data"); + let mint_b_ai = AccountInterface { + key: pdas.mint_b, + account: solana_account::Account { + lamports: 0, + data: vec![], + owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Account(compressed_b.clone())), + }; + + let specs: Vec> = vec![ + AccountSpec::Pda(record_spec), + AccountSpec::Pda(zc_spec), + AccountSpec::Ata(ata_interface), + AccountSpec::Pda(vault_spec), + AccountSpec::Mint(mint_a_ai), + AccountSpec::Mint(mint_b_ai), + ]; + + let decompress_ixs = + create_load_instructions(&specs, ctx.payer.pubkey(), ctx.config_pda, &ctx.rpc) + .await + .expect("create_load_instructions should succeed"); + + ctx.rpc + .create_and_send_transaction(&decompress_ixs, &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("Decompression should succeed"); + + // Verify all decompressed accounts exist on-chain + for (pda, name) in [ + (&pdas.record, "MinimalRecord"), + (&pdas.zc_record, "ZeroCopyRecord"), + (&pdas.ata, "ATA"), + (&pdas.vault, "Vault"), + (&pdas.mint_a, "MintA"), + (&pdas.mint_b, "MintB"), + ] { + shared::assert_onchain_exists(&mut ctx.rpc, pda, name).await; + } +} + +/// Compress all accounts by warping forward epochs +async fn compress_all(ctx: &mut StressTestContext, pdas: &TestPdas) { + ctx.rpc + .warp_slot_forward(SLOTS_PER_EPOCH * 100) + .await + .unwrap(); + + for (pda, name) in [ + (&pdas.record, "MinimalRecord"), + (&pdas.zc_record, "ZeroCopyRecord"), + (&pdas.ata, "ATA"), + (&pdas.vault, "Vault"), + (&pdas.mint_a, "MintA"), + (&pdas.mint_b, "MintB"), + ] { + shared::assert_onchain_closed(&mut ctx.rpc, pda, name).await; + } +} + +/// Full-struct assertions for all accounts against cached state +async fn assert_all_state( + rpc: &mut LightProgramTest, + pdas: &TestPdas, + cached: &CachedState, + iteration: usize, +) { + // MinimalRecord + let account = rpc.get_account(pdas.record).await.unwrap().unwrap(); + let actual_record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &account.data[8..]).unwrap(); + let expected_record = MinimalRecord { + compression_info: shared::expected_compression_info(&actual_record.compression_info), + ..cached.record.clone() + }; + assert_eq!( + actual_record, expected_record, + "MinimalRecord mismatch at iteration {iteration}" + ); + + // ZeroCopyRecord + let account = rpc.get_account(pdas.zc_record).await.unwrap().unwrap(); + let actual_zc: &ZeroCopyRecord = bytemuck::from_bytes(&account.data[8..]); + let expected_zc = ZeroCopyRecord { + compression_info: shared::expected_compression_info(&actual_zc.compression_info), + ..cached.zc_record + }; + assert_eq!( + *actual_zc, expected_zc, + "ZeroCopyRecord mismatch at iteration {iteration}" + ); + + // ATA + let actual_ata = parse_token(&rpc.get_account(pdas.ata).await.unwrap().unwrap().data); + let expected_ata = Token { + extensions: actual_ata.extensions.clone(), + ..cached.ata_token.clone() + }; + assert_eq!( + actual_ata, expected_ata, + "ATA mismatch at iteration {iteration}" + ); + + // Vault + let actual_vault = parse_token(&rpc.get_account(pdas.vault).await.unwrap().unwrap().data); + let expected_vault = Token { + extensions: actual_vault.extensions.clone(), + ..cached.vault_token.clone() + }; + assert_eq!( + actual_vault, expected_vault, + "Vault mismatch at iteration {iteration}" + ); + + // Mints + let actual_mint_a: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(pdas.mint_a).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!( + actual_mint_a.base.decimals, 9, + "Mint A decimals mismatch at iteration {iteration}" + ); + + let actual_mint_b: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(pdas.mint_b).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!( + actual_mint_b.base.decimals, 6, + "Mint B decimals mismatch at iteration {iteration}" + ); +} + +#[tokio::test] +async fn test_stress_20_iterations() { + let (mut ctx, pdas) = setup().await; + + // Verify initial creation + for (pda, name) in [ + (&pdas.record, "MinimalRecord"), + (&pdas.zc_record, "ZeroCopyRecord"), + (&pdas.ata, "ATA"), + (&pdas.vault, "Vault"), + (&pdas.mint_a, "MintA"), + (&pdas.mint_b, "MintB"), + ] { + shared::assert_onchain_exists(&mut ctx.rpc, pda, name).await; + } + + // Cache initial state + let owner = { + let account = ctx.rpc.get_account(pdas.record).await.unwrap().unwrap(); + let record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &account.data[8..]).unwrap(); + record.owner + }; + let mut cached = refresh_cache(&mut ctx.rpc, &pdas, owner).await; + + // First compression + compress_all(&mut ctx, &pdas).await; + + // Main loop: 20 iterations + for i in 0..20 { + println!("--- Iteration {i} ---"); + + // Decompress all + decompress_all(&mut ctx, &pdas, &cached).await; + + // Assert all cached state + assert_all_state(&mut ctx.rpc, &pdas, &cached, i).await; + + // Update cache after decompression (compression_info changes) + cached = refresh_cache(&mut ctx.rpc, &pdas, owner).await; + + // Compress all + compress_all(&mut ctx, &pdas).await; + + println!(" iteration {i} complete"); + } + + println!("All 20 iterations completed successfully."); +} diff --git a/sdk-tests/anchor-semi-manual-test/tests/test_create_all.rs b/sdk-tests/anchor-semi-manual-test/tests/test_create_all.rs new file mode 100644 index 0000000000..32dcc8b19b --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/tests/test_create_all.rs @@ -0,0 +1,442 @@ +mod shared; + +use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; +use anchor_semi_manual_test::{ + CreateAllParams, MinimalRecord, VaultSeeds, ZeroCopyRecord, MINT_SIGNER_SEED_A, + MINT_SIGNER_SEED_B, RECORD_SEED, VAULT_AUTH_SEED, VAULT_SEED, +}; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountInterfaceExt, + AccountSpec, ColdContext, CreateAccountsProofInput, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_token_interface::state::token::{AccountState, Token, ACCOUNT_TYPE_TOKEN_ACCOUNT}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_all_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + // Setup pre-existing mints for ATA and vault + let (ata_mint, _) = shared::setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9).await; + let (vault_mint, _) = shared::setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9).await; + + let owner = Keypair::new().pubkey(); + let authority = Keypair::new(); + + // PDA + let (record_pda, _) = + Pubkey::find_program_address(&[b"minimal_record", owner.as_ref()], &program_id); + + // Zero-copy + let (zc_record_pda, _) = + Pubkey::find_program_address(&[RECORD_SEED, owner.as_ref()], &program_id); + + // ATA + let ata_owner = payer.pubkey(); + let (ata, ata_bump) = light_token::instruction::derive_token_ata(&ata_owner, &ata_mint); + + // Token vault + let (vault_authority, _) = Pubkey::find_program_address(&[VAULT_AUTH_SEED], &program_id); + let (vault, vault_bump) = + Pubkey::find_program_address(&[VAULT_SEED, vault_mint.as_ref()], &program_id); + + // Mint A + let (mint_signer_a, mint_signer_bump_a) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_A, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_a_pda, _) = light_token::instruction::find_mint_address(&mint_signer_a); + + // Mint B + let (mint_signer_b, mint_signer_bump_b) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_B, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_b_pda, _) = light_token::instruction::find_mint_address(&mint_signer_b); + + // Build proof inputs for all accounts + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::pda(record_pda), + CreateAccountsProofInput::pda(zc_record_pda), + CreateAccountsProofInput::mint(mint_signer_a), + CreateAccountsProofInput::mint(mint_signer_b), + ], + ) + .await + .unwrap(); + + let accounts = anchor_semi_manual_test::accounts::CreateAll { + fee_payer: payer.pubkey(), + compression_config: env.config_pda, + pda_rent_sponsor: env.rent_sponsor, + record: record_pda, + zero_copy_record: zc_record_pda, + ata_mint, + ata_owner, + ata, + vault_mint, + vault_authority, + vault, + authority: authority.pubkey(), + mint_signer_a, + mint_a: mint_a_pda, + mint_signer_b, + mint_b: mint_b_pda, + light_token_config: LIGHT_TOKEN_CONFIG, + light_token_rent_sponsor: LIGHT_TOKEN_RENT_SPONSOR, + light_token_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = anchor_semi_manual_test::instruction::CreateAll { + params: CreateAllParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + ata_bump, + vault_bump, + mint_signer_bump_a, + mint_signer_bump_b, + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateAll should succeed"); + + // PHASE 1: Verify all accounts on-chain after creation + use light_compressed_account::pubkey::Pubkey as LPubkey; + + let record_account = rpc + .get_account(record_pda) + .await + .unwrap() + .expect("Record PDA should exist"); + let record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &record_account.data[8..]) + .expect("Failed to deserialize MinimalRecord"); + assert_eq!(record.owner, owner, "Record owner should match"); + + let zc_account = rpc + .get_account(zc_record_pda) + .await + .unwrap() + .expect("Zero-copy record should exist"); + let zc_record: &ZeroCopyRecord = bytemuck::from_bytes(&zc_account.data[8..]); + assert_eq!(zc_record.owner, owner, "ZC record owner should match"); + assert_eq!(zc_record.counter, 0, "ZC record counter should be 0"); + + let ata_account = rpc + .get_account(ata) + .await + .unwrap() + .expect("ATA should exist"); + let ata_token: Token = borsh::BorshDeserialize::deserialize(&mut &ata_account.data[..]) + .expect("Failed to deserialize ATA Token"); + assert_eq!( + ata_token.mint, + LPubkey::from(ata_mint.to_bytes()), + "ATA mint should match" + ); + assert_eq!( + ata_token.owner, + LPubkey::from(ata_owner.to_bytes()), + "ATA owner should match" + ); + + let vault_account = rpc + .get_account(vault) + .await + .unwrap() + .expect("Vault should exist"); + let vault_token: Token = borsh::BorshDeserialize::deserialize(&mut &vault_account.data[..]) + .expect("Failed to deserialize Vault Token"); + assert_eq!( + vault_token.mint, + LPubkey::from(vault_mint.to_bytes()), + "Vault mint should match" + ); + assert_eq!( + vault_token.owner, + LPubkey::from(vault_authority.to_bytes()), + "Vault owner should match" + ); + + use light_token_interface::state::Mint; + + let mint_a_account = rpc + .get_account(mint_a_pda) + .await + .unwrap() + .expect("Mint A should exist"); + let mint_a: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_a_account.data[..]) + .expect("Failed to deserialize Mint A"); + assert_eq!(mint_a.base.decimals, 9, "Mint A should have 9 decimals"); + + let mint_b_account = rpc + .get_account(mint_b_pda) + .await + .unwrap() + .expect("Mint B should exist"); + let mint_b: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_b_account.data[..]) + .expect("Failed to deserialize Mint B"); + assert_eq!(mint_b.base.decimals, 6, "Mint B should have 6 decimals"); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + + shared::assert_onchain_closed(&mut rpc, &record_pda, "MinimalRecord").await; + shared::assert_onchain_closed(&mut rpc, &zc_record_pda, "ZeroCopyRecord").await; + shared::assert_onchain_closed(&mut rpc, &ata, "ATA").await; + shared::assert_onchain_closed(&mut rpc, &vault, "Vault").await; + shared::assert_onchain_closed(&mut rpc, &mint_a_pda, "MintA").await; + shared::assert_onchain_closed(&mut rpc, &mint_b_pda, "MintB").await; + + // PHASE 3: Decompress all accounts via create_load_instructions. + use anchor_semi_manual_test::{LightAccountVariant, MinimalRecordSeeds, ZeroCopyRecordSeeds}; + + // PDA: MinimalRecord + let record_interface = rpc + .get_account_interface(&record_pda, &program_id) + .await + .expect("failed to get MinimalRecord interface"); + assert!(record_interface.is_cold(), "MinimalRecord should be cold"); + + let record_data = MinimalRecord::deserialize(&mut &record_interface.account.data[8..]) + .expect("Failed to parse MinimalRecord"); + let record_variant = LightAccountVariant::MinimalRecord { + seeds: MinimalRecordSeeds { owner }, + data: record_data, + }; + let record_spec = PdaSpec::new(record_interface, record_variant, program_id); + + // PDA: ZeroCopyRecord + let zc_interface = rpc + .get_account_interface(&zc_record_pda, &program_id) + .await + .expect("failed to get ZeroCopyRecord interface"); + assert!(zc_interface.is_cold(), "ZeroCopyRecord should be cold"); + + let zc_data = ZeroCopyRecord::deserialize(&mut &zc_interface.account.data[8..]) + .expect("Failed to parse ZeroCopyRecord"); + let zc_variant = LightAccountVariant::ZeroCopyRecord { + seeds: ZeroCopyRecordSeeds { owner }, + data: zc_data, + }; + let zc_spec = PdaSpec::new(zc_interface, zc_variant, program_id); + + // ATA + let ata_interface = rpc + .get_ata_interface(&ata_owner, &ata_mint) + .await + .expect("failed to get ATA interface"); + assert!(ata_interface.is_cold(), "ATA should be cold"); + + // Mint A + let mint_a_iface = rpc + .get_mint_interface(&mint_a_pda) + .await + .expect("failed to get mint A interface"); + assert!(mint_a_iface.is_cold(), "Mint A should be cold"); + let (compressed_a, _) = mint_a_iface + .compressed() + .expect("cold mint A must have compressed data"); + let mint_a_ai = AccountInterface { + key: mint_a_pda, + account: solana_account::Account { + lamports: 0, + data: vec![], + owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Account(compressed_a.clone())), + }; + + // Mint B + let mint_b_iface = rpc + .get_mint_interface(&mint_b_pda) + .await + .expect("failed to get mint B interface"); + assert!(mint_b_iface.is_cold(), "Mint B should be cold"); + let (compressed_b, _) = mint_b_iface + .compressed() + .expect("cold mint B must have compressed data"); + let mint_b_ai = AccountInterface { + key: mint_b_pda, + account: solana_account::Account { + lamports: 0, + data: vec![], + owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Account(compressed_b.clone())), + }; + + // Token PDA: Vault + let vault_iface = rpc + .get_token_account_interface(&vault) + .await + .expect("failed to get vault interface"); + assert!(vault_iface.is_cold(), "Vault should be cold"); + + let vault_token_data: Token = + borsh::BorshDeserialize::deserialize(&mut &vault_iface.account.data[..]) + .expect("Failed to parse vault Token"); + let vault_variant = LightAccountVariant::Vault(light_account::token::TokenDataWithSeeds { + seeds: VaultSeeds { mint: vault_mint }, + token_data: vault_token_data, + }); + let vault_compressed = vault_iface + .compressed() + .expect("cold vault must have compressed data"); + let vault_interface = AccountInterface { + key: vault_iface.key, + account: vault_iface.account.clone(), + cold: Some(ColdContext::Account(vault_compressed.account.clone())), + }; + let vault_spec = PdaSpec::new(vault_interface, vault_variant, program_id); + + let specs: Vec> = vec![ + AccountSpec::Pda(record_spec), + AccountSpec::Pda(zc_spec), + AccountSpec::Ata(ata_interface), + AccountSpec::Pda(vault_spec), + AccountSpec::Mint(mint_a_ai), + AccountSpec::Mint(mint_b_ai), + ]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &record_pda, "MinimalRecord").await; + shared::assert_onchain_exists(&mut rpc, &zc_record_pda, "ZeroCopyRecord").await; + shared::assert_onchain_exists(&mut rpc, &ata, "ATA").await; + shared::assert_onchain_exists(&mut rpc, &vault, "Vault").await; + shared::assert_onchain_exists(&mut rpc, &mint_a_pda, "MintA").await; + shared::assert_onchain_exists(&mut rpc, &mint_b_pda, "MintB").await; + + // MinimalRecord + let account = rpc.get_account(record_pda).await.unwrap().unwrap(); + let actual_record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &account.data[8..]).unwrap(); + let expected_record = MinimalRecord { + compression_info: shared::expected_compression_info(&actual_record.compression_info), + owner, + }; + assert_eq!( + actual_record, expected_record, + "MinimalRecord should match after decompression" + ); + + // ZeroCopyRecord + let account = rpc.get_account(zc_record_pda).await.unwrap().unwrap(); + let actual_zc: &ZeroCopyRecord = bytemuck::from_bytes(&account.data[8..]); + let expected_zc = ZeroCopyRecord { + compression_info: shared::expected_compression_info(&actual_zc.compression_info), + owner, + counter: 0, + }; + assert_eq!( + *actual_zc, expected_zc, + "ZeroCopyRecord should match after decompression" + ); + + // ATA + let actual_ata: Token = shared::parse_token(&rpc.get_account(ata).await.unwrap().unwrap().data); + let expected_ata = Token { + mint: LPubkey::from(ata_mint.to_bytes()), + owner: LPubkey::from(ata_owner.to_bytes()), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: actual_ata.extensions.clone(), + }; + assert_eq!( + actual_ata, expected_ata, + "ATA should match after decompression" + ); + + // Vault + let actual_vault: Token = + shared::parse_token(&rpc.get_account(vault).await.unwrap().unwrap().data); + let expected_vault = Token { + mint: LPubkey::from(vault_mint.to_bytes()), + owner: LPubkey::from(vault_authority.to_bytes()), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: actual_vault.extensions.clone(), + }; + assert_eq!( + actual_vault, expected_vault, + "Vault should match after decompression" + ); + + // Mints + let actual_ma: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(mint_a_pda).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!( + actual_ma.base.decimals, 9, + "Mint A decimals should be preserved" + ); + assert_eq!( + actual_ma.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint A authority should be preserved" + ); + + let actual_mb: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(mint_b_pda).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!( + actual_mb.base.decimals, 6, + "Mint B decimals should be preserved" + ); + assert_eq!( + actual_mb.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint B authority should be preserved" + ); +} diff --git a/sdk-tests/anchor-semi-manual-test/tests/test_create_ata.rs b/sdk-tests/anchor-semi-manual-test/tests/test_create_ata.rs new file mode 100644 index 0000000000..3f7ff9883b --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/tests/test_create_ata.rs @@ -0,0 +1,132 @@ +mod shared; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anchor_semi_manual_test::CreateAtaParams; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use solana_instruction::Instruction; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_ata_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let (mint, _mint_seed) = shared::setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9).await; + + let ata_owner = payer.pubkey(); + let (ata, ata_bump) = light_token::instruction::derive_token_ata(&ata_owner, &mint); + + let proof_result = get_create_accounts_proof(&rpc, &program_id, vec![]) + .await + .unwrap(); + + let accounts = anchor_semi_manual_test::accounts::CreateAta { + fee_payer: payer.pubkey(), + ata_mint: mint, + ata_owner, + ata, + light_token_config: LIGHT_TOKEN_CONFIG, + light_token_rent_sponsor: LIGHT_TOKEN_RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = anchor_semi_manual_test::instruction::CreateAta { + params: CreateAtaParams { + create_accounts_proof: proof_result.create_accounts_proof, + ata_bump, + }, + }; + + 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]) + .await + .expect("CreateAta should succeed"); + + // PHASE 1: Verify on-chain after creation + let ata_account = rpc + .get_account(ata) + .await + .unwrap() + .expect("ATA should exist on-chain"); + + use light_token_interface::state::token::{AccountState, Token, ACCOUNT_TYPE_TOKEN_ACCOUNT}; + let token: Token = borsh::BorshDeserialize::deserialize(&mut &ata_account.data[..]) + .expect("Failed to deserialize Token"); + + let expected_token = Token { + mint: mint.to_bytes().into(), + owner: ata_owner.to_bytes().into(), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: token.extensions.clone(), + }; + + assert_eq!( + token, expected_token, + "ATA should match expected after creation" + ); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &ata, "ATA").await; + + // PHASE 3: Decompress via create_load_instructions + use anchor_semi_manual_test::LightAccountVariant; + + let ata_interface = rpc + .get_ata_interface(&ata_owner, &mint) + .await + .expect("failed to get ATA interface"); + assert!(ata_interface.is_cold(), "ATA should be cold"); + + let specs: Vec> = vec![AccountSpec::Ata(ata_interface)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &ata, "ATA").await; + + let actual: Token = shared::parse_token(&rpc.get_account(ata).await.unwrap().unwrap().data); + let expected = Token { + mint: mint.to_bytes().into(), + owner: ata_owner.to_bytes().into(), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: actual.extensions.clone(), + }; + assert_eq!(actual, expected, "ATA should match after decompression"); +} diff --git a/sdk-tests/anchor-semi-manual-test/tests/test_create_mint.rs b/sdk-tests/anchor-semi-manual-test/tests/test_create_mint.rs new file mode 100644 index 0000000000..8af51ab83e --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/tests/test_create_mint.rs @@ -0,0 +1,146 @@ +mod shared; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anchor_semi_manual_test::{CreateMintParams, MINT_SIGNER_SEED_A}; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountInterfaceExt, + AccountSpec, ColdContext, CreateAccountsProofInput, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_mint_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let authority = Keypair::new(); + + let (mint_signer_pda, mint_signer_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_A, authority.pubkey().as_ref()], + &program_id, + ); + + let (mint_pda, _) = light_token::instruction::find_mint_address(&mint_signer_pda); + + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::mint(mint_signer_pda)], + ) + .await + .unwrap(); + + let accounts = anchor_semi_manual_test::accounts::CreateMint { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_signer: mint_signer_pda, + mint: mint_pda, + compression_config: env.config_pda, + light_token_config: LIGHT_TOKEN_CONFIG, + light_token_rent_sponsor: LIGHT_TOKEN_RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + light_token_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = anchor_semi_manual_test::instruction::CreateMint { + params: CreateMintParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump, + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateMint should succeed"); + + // PHASE 1: Verify on-chain after creation + let mint_account = rpc + .get_account(mint_pda) + .await + .unwrap() + .expect("Mint should exist on-chain"); + + use light_token_interface::state::Mint; + let mint: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_account.data[..]) + .expect("Failed to deserialize Mint"); + + assert_eq!(mint.base.decimals, 9, "Mint should have 9 decimals"); + assert_eq!( + mint.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint authority should be fee_payer" + ); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &mint_pda, "Mint").await; + + // PHASE 3: Decompress via create_load_instructions + use anchor_semi_manual_test::LightAccountVariant; + + let mint_interface = rpc + .get_mint_interface(&mint_pda) + .await + .expect("failed to get mint interface"); + assert!(mint_interface.is_cold(), "Mint should be cold"); + + let (compressed, _mint_data) = mint_interface + .compressed() + .expect("cold mint must have compressed data"); + let mint_account_interface = AccountInterface { + key: mint_pda, + account: solana_account::Account { + lamports: 0, + data: vec![], + owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Account(compressed.clone())), + }; + + let specs: Vec> = + vec![AccountSpec::Mint(mint_account_interface)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &mint_pda, "Mint").await; + + let actual: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(mint_pda).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!(actual.base.decimals, 9, "Mint decimals should be preserved"); + assert_eq!( + actual.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint authority should be preserved" + ); +} diff --git a/sdk-tests/anchor-semi-manual-test/tests/test_create_pda.rs b/sdk-tests/anchor-semi-manual-test/tests/test_create_pda.rs new file mode 100644 index 0000000000..06b0818f39 --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/tests/test_create_pda.rs @@ -0,0 +1,131 @@ +mod shared; + +use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; +use anchor_semi_manual_test::CreatePdaParams; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, + CreateAccountsProofInput, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_single_pda_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let owner = Keypair::new().pubkey(); + + let (record_pda, _) = + Pubkey::find_program_address(&[b"minimal_record", owner.as_ref()], &program_id); + + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::pda(record_pda)], + ) + .await + .unwrap(); + + let accounts = anchor_semi_manual_test::accounts::CreatePda { + fee_payer: payer.pubkey(), + compression_config: env.config_pda, + pda_rent_sponsor: env.rent_sponsor, + record: record_pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = anchor_semi_manual_test::instruction::CreatePda { + params: CreatePdaParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + 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]) + .await + .expect("CreatePda should succeed"); + + // PHASE 1: Verify on-chain after creation + let record_account = rpc + .get_account(record_pda) + .await + .unwrap() + .expect("Record PDA should exist on-chain"); + + use anchor_semi_manual_test::MinimalRecord; + let record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &record_account.data[8..]) + .expect("Failed to deserialize MinimalRecord"); + + let expected = MinimalRecord { + compression_info: shared::expected_compression_info(&record.compression_info), + owner, + }; + assert_eq!( + record, expected, + "MinimalRecord should match after creation" + ); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &record_pda, "MinimalRecord").await; + + // PHASE 3: Decompress via create_load_instructions + use anchor_semi_manual_test::{LightAccountVariant, MinimalRecordSeeds}; + + let account_interface = rpc + .get_account_interface(&record_pda, &program_id) + .await + .expect("failed to get MinimalRecord interface"); + assert!(account_interface.is_cold(), "MinimalRecord should be cold"); + + let data = MinimalRecord::deserialize(&mut &account_interface.account.data[8..]) + .expect("Failed to parse MinimalRecord from interface"); + let variant = LightAccountVariant::MinimalRecord { + seeds: MinimalRecordSeeds { owner }, + data, + }; + + let spec = PdaSpec::new(account_interface, variant, program_id); + let specs: Vec> = vec![AccountSpec::Pda(spec)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &record_pda, "MinimalRecord").await; + + let account = rpc.get_account(record_pda).await.unwrap().unwrap(); + let actual: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &account.data[8..]).unwrap(); + let expected = MinimalRecord { + compression_info: shared::expected_compression_info(&actual.compression_info), + owner, + }; + assert_eq!( + actual, expected, + "MinimalRecord should match after decompression" + ); +} diff --git a/sdk-tests/anchor-semi-manual-test/tests/test_create_token_vault.rs b/sdk-tests/anchor-semi-manual-test/tests/test_create_token_vault.rs new file mode 100644 index 0000000000..ba64ff98dd --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/tests/test_create_token_vault.rs @@ -0,0 +1,169 @@ +mod shared; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anchor_semi_manual_test::{ + CreateTokenVaultParams, LightAccountVariant, VaultSeeds, VAULT_AUTH_SEED, VAULT_SEED, +}; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountInterfaceExt, + AccountSpec, ColdContext, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_token_interface::state::token::{AccountState, Token, ACCOUNT_TYPE_TOKEN_ACCOUNT}; +use solana_instruction::Instruction; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Token vault lifecycle: create -> verify on-chain -> warp -> verify compressed -> decompress -> verify restored. +#[tokio::test] +async fn test_create_token_vault_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let (mint, _mint_seed) = shared::setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9).await; + + let (vault_authority, _auth_bump) = + Pubkey::find_program_address(&[VAULT_AUTH_SEED], &program_id); + let (vault, vault_bump) = + Pubkey::find_program_address(&[VAULT_SEED, mint.as_ref()], &program_id); + + let proof_result = get_create_accounts_proof(&rpc, &program_id, vec![]) + .await + .unwrap(); + + let accounts = anchor_semi_manual_test::accounts::CreateTokenVault { + fee_payer: payer.pubkey(), + mint, + vault_authority, + vault, + light_token_config: LIGHT_TOKEN_CONFIG, + light_token_rent_sponsor: LIGHT_TOKEN_RENT_SPONSOR, + light_token_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = anchor_semi_manual_test::instruction::CreateTokenVault { + params: CreateTokenVaultParams { + create_accounts_proof: proof_result.create_accounts_proof, + vault_bump, + }, + }; + + 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]) + .await + .expect("CreateTokenVault should succeed"); + + // PHASE 1: Verify on-chain after creation + let vault_account = rpc + .get_account(vault) + .await + .unwrap() + .expect("Token vault should exist on-chain"); + + let token: Token = borsh::BorshDeserialize::deserialize(&mut &vault_account.data[..]) + .expect("Failed to deserialize Token"); + + let expected_token = Token { + mint: mint.to_bytes().into(), + owner: vault_authority.to_bytes().into(), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: token.extensions.clone(), + }; + + assert_eq!( + token, expected_token, + "Token vault should match expected after creation" + ); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &vault, "Vault").await; + + // PHASE 3: Decompress vault + let vault_iface = rpc + .get_token_account_interface(&vault) + .await + .expect("failed to get vault interface"); + assert!(vault_iface.is_cold(), "Vault should be cold"); + + let token_data: Token = + borsh::BorshDeserialize::deserialize(&mut &vault_iface.account.data[..]) + .expect("Failed to parse vault Token"); + let vault_variant = LightAccountVariant::Vault(light_account::token::TokenDataWithSeeds { + seeds: VaultSeeds { mint }, + token_data, + }); + let vault_compressed = vault_iface + .compressed() + .expect("cold vault must have compressed data"); + let vault_interface = AccountInterface { + key: vault_iface.key, + account: vault_iface.account.clone(), + cold: Some(ColdContext::Account(vault_compressed.account.clone())), + }; + let vault_spec = PdaSpec::new(vault_interface, vault_variant, program_id); + + let specs: Vec> = vec![AccountSpec::Pda(vault_spec)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Vault decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &vault, "Vault").await; + + let vault_account = rpc + .get_account(vault) + .await + .unwrap() + .expect("Vault should exist on-chain after decompression"); + + let actual_token: Token = borsh::BorshDeserialize::deserialize(&mut &vault_account.data[..]) + .expect("Failed to deserialize Token after decompression"); + + use light_compressed_account::pubkey::Pubkey as LPubkey; + + let expected_token = Token { + mint: LPubkey::from(mint.to_bytes()), + owner: LPubkey::from(vault_authority.to_bytes()), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: actual_token.extensions.clone(), + }; + + assert_eq!( + actual_token, expected_token, + "Token vault should match expected after decompression" + ); +} diff --git a/sdk-tests/anchor-semi-manual-test/tests/test_create_two_mints.rs b/sdk-tests/anchor-semi-manual-test/tests/test_create_two_mints.rs new file mode 100644 index 0000000000..4ac2f0dfb3 --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/tests/test_create_two_mints.rs @@ -0,0 +1,199 @@ +mod shared; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anchor_semi_manual_test::{CreateTwoMintsParams, MINT_SIGNER_SEED_A, MINT_SIGNER_SEED_B}; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountInterfaceExt, + AccountSpec, ColdContext, CreateAccountsProofInput, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_two_mints_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let authority = Keypair::new(); + + let (mint_signer_a, mint_signer_bump_a) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_A, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_a_pda, _) = light_token::instruction::find_mint_address(&mint_signer_a); + + let (mint_signer_b, mint_signer_bump_b) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_B, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_b_pda, _) = light_token::instruction::find_mint_address(&mint_signer_b); + + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::mint(mint_signer_a), + CreateAccountsProofInput::mint(mint_signer_b), + ], + ) + .await + .unwrap(); + + let accounts = anchor_semi_manual_test::accounts::CreateTwoMints { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_signer_a, + mint_a: mint_a_pda, + mint_signer_b, + mint_b: mint_b_pda, + compression_config: env.config_pda, + light_token_config: LIGHT_TOKEN_CONFIG, + light_token_rent_sponsor: LIGHT_TOKEN_RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + light_token_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = anchor_semi_manual_test::instruction::CreateTwoMints { + params: CreateTwoMintsParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump_a, + mint_signer_bump_b, + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateTwoMints should succeed"); + + // PHASE 1: Verify on-chain after creation + use light_token_interface::state::Mint; + + let mint_a_account = rpc + .get_account(mint_a_pda) + .await + .unwrap() + .expect("Mint A should exist on-chain"); + let mint_a: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_a_account.data[..]) + .expect("Failed to deserialize Mint A"); + assert_eq!(mint_a.base.decimals, 9, "Mint A should have 9 decimals"); + assert_eq!( + mint_a.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint A authority should be fee_payer" + ); + + let mint_b_account = rpc + .get_account(mint_b_pda) + .await + .unwrap() + .expect("Mint B should exist on-chain"); + let mint_b: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_b_account.data[..]) + .expect("Failed to deserialize Mint B"); + assert_eq!(mint_b.base.decimals, 6, "Mint B should have 6 decimals"); + assert_eq!( + mint_b.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint B authority should be fee_payer" + ); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &mint_a_pda, "MintA").await; + shared::assert_onchain_closed(&mut rpc, &mint_b_pda, "MintB").await; + + // PHASE 3: Decompress both mints via create_load_instructions + use anchor_semi_manual_test::LightAccountVariant; + + let build_mint_account_interface = |mint_interface: light_client::interface::MintInterface| { + let (compressed, _mint_data) = mint_interface + .compressed() + .expect("cold mint must have compressed data"); + AccountInterface { + key: mint_interface.mint, + account: solana_account::Account { + lamports: 0, + data: vec![], + owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Account(compressed.clone())), + } + }; + + let mint_a_interface = rpc + .get_mint_interface(&mint_a_pda) + .await + .expect("failed to get mint A interface"); + assert!(mint_a_interface.is_cold(), "Mint A should be cold"); + let mint_a_ai = build_mint_account_interface(mint_a_interface); + + let mint_b_interface = rpc + .get_mint_interface(&mint_b_pda) + .await + .expect("failed to get mint B interface"); + assert!(mint_b_interface.is_cold(), "Mint B should be cold"); + let mint_b_ai = build_mint_account_interface(mint_b_interface); + + let specs: Vec> = + vec![AccountSpec::Mint(mint_a_ai), AccountSpec::Mint(mint_b_ai)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &mint_a_pda, "MintA").await; + shared::assert_onchain_exists(&mut rpc, &mint_b_pda, "MintB").await; + + let actual_a: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(mint_a_pda).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!( + actual_a.base.decimals, 9, + "Mint A decimals should be preserved" + ); + assert_eq!( + actual_a.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint A authority should be preserved" + ); + + let actual_b: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(mint_b_pda).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!( + actual_b.base.decimals, 6, + "Mint B decimals should be preserved" + ); + assert_eq!( + actual_b.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint B authority should be preserved" + ); +} diff --git a/sdk-tests/anchor-semi-manual-test/tests/test_create_zero_copy_record.rs b/sdk-tests/anchor-semi-manual-test/tests/test_create_zero_copy_record.rs new file mode 100644 index 0000000000..0655117ac6 --- /dev/null +++ b/sdk-tests/anchor-semi-manual-test/tests/test_create_zero_copy_record.rs @@ -0,0 +1,125 @@ +mod shared; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use anchor_semi_manual_test::{CreateZeroCopyRecordParams, RECORD_SEED}; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, + CreateAccountsProofInput, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_zero_copy_record_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let owner = Keypair::new().pubkey(); + + let (record_pda, _) = Pubkey::find_program_address(&[RECORD_SEED, owner.as_ref()], &program_id); + + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::pda(record_pda)], + ) + .await + .unwrap(); + + let accounts = anchor_semi_manual_test::accounts::CreateZeroCopyRecord { + fee_payer: payer.pubkey(), + compression_config: env.config_pda, + pda_rent_sponsor: env.rent_sponsor, + record: record_pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = anchor_semi_manual_test::instruction::CreateZeroCopyRecord { + params: CreateZeroCopyRecordParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + 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]) + .await + .expect("CreateZeroCopyRecord should succeed"); + + // PHASE 1: Verify on-chain after creation + let record_account = rpc + .get_account(record_pda) + .await + .unwrap() + .expect("Record PDA should exist on-chain"); + + use anchor_semi_manual_test::ZeroCopyRecord; + let discriminator_len = 8; + let data = &record_account.data[discriminator_len..]; + let record: &ZeroCopyRecord = bytemuck::from_bytes(data); + + assert_eq!(record.owner, owner, "Record owner should match"); + assert_eq!(record.counter, 0, "Record counter should be 0"); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &record_pda, "ZeroCopyRecord").await; + + // PHASE 3: Decompress via create_load_instructions + use anchor_lang::AnchorDeserialize; + use anchor_semi_manual_test::{LightAccountVariant, ZeroCopyRecordSeeds}; + + let account_interface = rpc + .get_account_interface(&record_pda, &program_id) + .await + .expect("failed to get ZeroCopyRecord interface"); + assert!(account_interface.is_cold(), "ZeroCopyRecord should be cold"); + + let zc_data = ZeroCopyRecord::deserialize(&mut &account_interface.account.data[8..]) + .expect("Failed to parse ZeroCopyRecord from interface"); + let variant = LightAccountVariant::ZeroCopyRecord { + seeds: ZeroCopyRecordSeeds { owner }, + data: zc_data, + }; + + let spec = PdaSpec::new(account_interface, variant, program_id); + let specs: Vec> = vec![AccountSpec::Pda(spec)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &record_pda, "ZeroCopyRecord").await; + + let account = rpc.get_account(record_pda).await.unwrap().unwrap(); + let actual: &ZeroCopyRecord = bytemuck::from_bytes(&account.data[8..]); + let expected = ZeroCopyRecord { + compression_info: shared::expected_compression_info(&actual.compression_info), + owner, + counter: 0, + }; + assert_eq!( + *actual, expected, + "ZeroCopyRecord should match after decompression" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test-sdk/Cargo.toml b/sdk-tests/csdk-anchor-full-derived-test-sdk/Cargo.toml index 25dcb7f470..0b9d952940 100644 --- a/sdk-tests/csdk-anchor-full-derived-test-sdk/Cargo.toml +++ b/sdk-tests/csdk-anchor-full-derived-test-sdk/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" csdk-anchor-full-derived-test = { path = "../csdk-anchor-full-derived-test", features = ["no-entrypoint"] } # SDK trait and types +light-account = { workspace = true, features = ["token", "anchor"] } light-client = { workspace = true, features = ["v2", "anchor"] } light-sdk = { workspace = true, features = ["anchor", "v2"] } light-token = { workspace = true, features = ["anchor"] } diff --git a/sdk-tests/csdk-anchor-full-derived-test-sdk/src/lib.rs b/sdk-tests/csdk-anchor-full-derived-test-sdk/src/lib.rs index 5cbe8e3521..308e4aa52f 100644 --- a/sdk-tests/csdk-anchor-full-derived-test-sdk/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test-sdk/src/lib.rs @@ -190,7 +190,7 @@ impl AmmSdk { account: &AccountInterface, is_vault_0: bool, ) -> Result<(), AmmSdkError> { - use light_sdk::interface::token::{Token, TokenDataWithSeeds}; + use light_account::{token::TokenDataWithSeeds, Token}; let pool_state = self .pool_state_pubkey diff --git a/sdk-tests/csdk-anchor-full-derived-test-sdk/tests/trait_tests.rs b/sdk-tests/csdk-anchor-full-derived-test-sdk/tests/trait_tests.rs index 3a9c8332e2..4dcb0a8267 100644 --- a/sdk-tests/csdk-anchor-full-derived-test-sdk/tests/trait_tests.rs +++ b/sdk-tests/csdk-anchor-full-derived-test-sdk/tests/trait_tests.rs @@ -550,18 +550,18 @@ fn test_variant_seed_values_distinguish_instances() { use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ Token0VaultSeeds, Token1VaultSeeds, }; - use light_sdk::interface::token::TokenDataWithSeeds; + use light_account::token::TokenDataWithSeeds; let pool_state = Pubkey::new_unique(); let token_0_mint = Pubkey::new_unique(); let token_1_mint = Pubkey::new_unique(); - let default_token = light_sdk::interface::token::Token { + let default_token = light_account::token::Token { mint: Default::default(), owner: Default::default(), amount: 0, delegate: None, - state: light_sdk::interface::token::AccountState::Initialized, + state: light_account::token::AccountState::Initialized, is_native: None, delegated_amount: 0, close_authority: None, diff --git a/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml b/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml index b68c205f24..ef0384f659 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml +++ b/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml @@ -13,15 +13,15 @@ no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] -custom-heap = ["light-heap", "light-sdk/custom-heap"] default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build", "light-anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build", "light-anchor-spl/idl-build"] test-sbf = [] [dependencies] -light-heap = { workspace = true, optional = true } -light-sdk = { workspace = true, features = ["anchor", "v2", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +light-account = { workspace = true, features = ["token", "anchor", "sha256"] } +light-token = { workspace = true, features = ["anchor"] } +light-token-interface = { workspace = true } +light-token-types = { workspace = true } light-hasher = { workspace = true, features = ["solana"] } bytemuck = { workspace = true, features = ["derive"] } solana-program = { workspace = true } @@ -33,16 +33,10 @@ light-macros = { workspace = true, features = ["solana"] } light-instruction-decoder = { workspace = true } light-instruction-decoder-derive = { workspace = true } solana-instruction = { workspace = true } -light-sdk-macros = { workspace = true } borsh = { workspace = true } light-compressed-account = { workspace = true, features = ["solana"] } anchor-lang = { workspace = true } light-anchor-spl = { workspace = true, features = ["metadata"] } -light-token-interface = { workspace = true, features = ["anchor"] } -light-token = { workspace = true, features = ["anchor"] } -light-compressed-token-sdk = { workspace = true, features = ["anchor"] } -light-token-types = { workspace = true, features = ["anchor"] } -light-compressible = { workspace = true, features = ["anchor"] } [dev-dependencies] csdk-anchor-full-derived-test-sdk = { path = "../csdk-anchor-full-derived-test-sdk" } @@ -50,6 +44,9 @@ light-token-client = { workspace = true } light-program-test = { workspace = true, features = ["devenv"] } light-client = { workspace = true, features = ["v2", "anchor"] } light-test-utils = { workspace = true } +light-compressible = { workspace = true } +light-compressed-token-sdk = { workspace = true } +light-sdk-types = { workspace = true } tokio = { workspace = true } solana-sdk = { workspace = true } solana-logger = { workspace = true } @@ -63,6 +60,7 @@ bincode = "1.3" sha2 = { workspace = true } rand = { workspace = true } light-batched-merkle-tree = { workspace = true } +light-sdk = { workspace = true } [lints.rust.unexpected_cfgs] level = "allow" diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs index da246ce61f..eec8c1c643 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs @@ -8,13 +8,12 @@ //! - MintToCpi use anchor_lang::prelude::*; -use light_anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; -use light_token::instruction::{ - CreateTokenAccountCpi, CreateTokenAtaCpi, MintToCpi, LIGHT_TOKEN_CONFIG, - LIGHT_TOKEN_RENT_SPONSOR, +use light_account::{ + CreateAccountsProof, CreateTokenAccountCpi, CreateTokenAtaCpi, LightAccounts, + LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR, }; +use light_anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use light_token::instruction::MintToCpi; use super::states::*; @@ -173,60 +172,81 @@ pub fn process_initialize_pool<'info>( params: InitializeParams, ) -> Result<()> { // Create token_0_vault using CreateTokenAccountCpi (mark-only field) - CreateTokenAccountCpi { - payer: ctx.accounts.creator.to_account_info(), - account: ctx.accounts.token_0_vault.to_account_info(), - mint: ctx.accounts.token_0_mint.to_account_info(), - owner: ctx.accounts.authority.key(), + { + let payer_info = ctx.accounts.creator.to_account_info(); + let account_info = ctx.accounts.token_0_vault.to_account_info(); + let mint_info = ctx.accounts.token_0_mint.to_account_info(); + let config_info = ctx.accounts.light_token_config.to_account_info(); + let sponsor_info = ctx.accounts.light_token_rent_sponsor.to_account_info(); + let system_info = ctx.accounts.system_program.to_account_info(); + CreateTokenAccountCpi { + payer: &payer_info, + account: &account_info, + mint: &mint_info, + owner: ctx.accounts.authority.key().to_bytes(), + } + .rent_free( + &config_info, + &sponsor_info, + &system_info, + &crate::ID.to_bytes(), + ) + .invoke_signed(&[ + POOL_VAULT_SEED.as_bytes(), + ctx.accounts.pool_state.to_account_info().key.as_ref(), + ctx.accounts.token_0_mint.to_account_info().key.as_ref(), + &[ctx.bumps.token_0_vault], + ])?; } - .rent_free( - ctx.accounts.light_token_config.to_account_info(), - ctx.accounts.light_token_rent_sponsor.to_account_info(), - ctx.accounts.system_program.to_account_info(), - &crate::ID, - ) - .invoke_signed(&[ - POOL_VAULT_SEED.as_bytes(), - ctx.accounts.pool_state.to_account_info().key.as_ref(), - ctx.accounts.token_0_mint.to_account_info().key.as_ref(), - &[ctx.bumps.token_0_vault], - ])?; // Create token_1_vault using CreateTokenAccountCpi (mark-only field) - CreateTokenAccountCpi { - payer: ctx.accounts.creator.to_account_info(), - account: ctx.accounts.token_1_vault.to_account_info(), - mint: ctx.accounts.token_1_mint.to_account_info(), - owner: ctx.accounts.authority.key(), + { + let payer_info = ctx.accounts.creator.to_account_info(); + let account_info = ctx.accounts.token_1_vault.to_account_info(); + let mint_info = ctx.accounts.token_1_mint.to_account_info(); + let config_info = ctx.accounts.light_token_config.to_account_info(); + let sponsor_info = ctx.accounts.light_token_rent_sponsor.to_account_info(); + let system_info = ctx.accounts.system_program.to_account_info(); + CreateTokenAccountCpi { + payer: &payer_info, + account: &account_info, + mint: &mint_info, + owner: ctx.accounts.authority.key().to_bytes(), + } + .rent_free( + &config_info, + &sponsor_info, + &system_info, + &crate::ID.to_bytes(), + ) + .invoke_signed(&[ + POOL_VAULT_SEED.as_bytes(), + ctx.accounts.pool_state.to_account_info().key.as_ref(), + ctx.accounts.token_1_mint.to_account_info().key.as_ref(), + &[ctx.bumps.token_1_vault], + ])?; } - .rent_free( - ctx.accounts.light_token_config.to_account_info(), - ctx.accounts.light_token_rent_sponsor.to_account_info(), - ctx.accounts.system_program.to_account_info(), - &crate::ID, - ) - .invoke_signed(&[ - POOL_VAULT_SEED.as_bytes(), - ctx.accounts.pool_state.to_account_info().key.as_ref(), - ctx.accounts.token_1_mint.to_account_info().key.as_ref(), - &[ctx.bumps.token_1_vault], - ])?; // Create creator LP token ATA using CreateTokenAtaCpi.rent_free() - CreateTokenAtaCpi { - payer: ctx.accounts.creator.to_account_info(), - owner: ctx.accounts.creator.to_account_info(), - mint: ctx.accounts.lp_mint.to_account_info(), - ata: ctx.accounts.creator_lp_token.to_account_info(), - bump: params.creator_lp_token_bump, + { + let payer_info = ctx.accounts.creator.to_account_info(); + let owner_info = ctx.accounts.creator.to_account_info(); + let mint_info = ctx.accounts.lp_mint.to_account_info(); + let ata_info = ctx.accounts.creator_lp_token.to_account_info(); + let config_info = ctx.accounts.light_token_config.to_account_info(); + let sponsor_info = ctx.accounts.light_token_rent_sponsor.to_account_info(); + let system_info = ctx.accounts.system_program.to_account_info(); + CreateTokenAtaCpi { + payer: &payer_info, + owner: &owner_info, + mint: &mint_info, + ata: &ata_info, + bump: params.creator_lp_token_bump, + } + .idempotent() + .rent_free(&config_info, &sponsor_info, &system_info) + .invoke()?; } - .idempotent() - .rent_free( - ctx.accounts.light_token_config.to_account_info(), - ctx.accounts.light_token_rent_sponsor.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ) - .invoke()?; // Mint LP tokens using MintToCpi let lp_amount = 1000u64; // Placeholder amount diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs index 8a27af823d..1c12751032 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs @@ -1,8 +1,7 @@ //! AMM state structs adapted from cp-swap-reference. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; pub const POOL_SEED: &str = "pool"; pub const POOL_VAULT_SEED: &str = "pool_vault"; 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 adccbac8a6..85df44befa 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,6 +1,5 @@ use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::*; @@ -310,7 +309,7 @@ pub struct CreateMintWithMetadataParams { pub name: Vec, pub symbol: Vec, pub uri: Vec, - pub additional_metadata: Option>, + pub additional_metadata: Option>, } /// Test instruction with #[light_account(init)] with metadata fields. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_ata.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_ata.rs index 361686d52b..7ba4017a37 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_ata.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_ata.rs @@ -7,10 +7,10 @@ //! Here the macro should generate CreateTokenAtaCpi call automatically. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; -use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{ + CreateAccountsProof, LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_PROGRAM_ID, + LIGHT_TOKEN_RENT_SPONSOR, +}; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct D10SingleAtaParams { @@ -45,7 +45,7 @@ pub struct D10SingleAta<'info> { pub light_token_rent_sponsor: AccountInfo<'info>, /// CHECK: Light Token Program for CPI - #[account(address = LIGHT_TOKEN_PROGRAM_ID.into())] + #[account(address = LIGHT_TOKEN_PROGRAM_ID)] pub light_token_program: AccountInfo<'info>, pub system_program: Program<'info, System>, diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_ata_markonly.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_ata_markonly.rs index f825d97a7e..ff3ba2ff08 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_ata_markonly.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_ata_markonly.rs @@ -6,9 +6,9 @@ //! User manually calls CreateTokenAtaCpi in the instruction handler. use anchor_lang::prelude::*; -use light_sdk_macros::LightAccounts; -use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{ + LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_PROGRAM_ID, LIGHT_TOKEN_RENT_SPONSOR, +}; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct D10SingleAtaMarkonlyParams { @@ -44,7 +44,7 @@ pub struct D10SingleAtaMarkonly<'info> { pub light_token_rent_sponsor: AccountInfo<'info>, /// CHECK: Light Token Program for CPI - #[account(address = LIGHT_TOKEN_PROGRAM_ID.into())] + #[account(address = LIGHT_TOKEN_PROGRAM_ID)] pub light_token_program: AccountInfo<'info>, pub system_program: Program<'info, System>, diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_vault.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_vault.rs index 7c4f917ebc..b2fed5fda1 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_vault.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d10_token_accounts/single_vault.rs @@ -7,9 +7,9 @@ //! Here the macro should generate the CreateTokenAccountCpi call automatically. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{ + CreateAccountsProof, LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR, +}; /// Seed for the vault authority PDA pub const D10_SINGLE_VAULT_AUTH_SEED: &[u8] = b"d10_single_vault_auth"; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/mixed_zc_borsh.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/mixed_zc_borsh.rs index 53f3349d94..57ab306d69 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/mixed_zc_borsh.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/mixed_zc_borsh.rs @@ -4,8 +4,7 @@ //! Verifies that mixed serialization types work together in the same instruction. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::{ d11_zero_copy::ZcBasicRecord, d1_field_types::single_pubkey::SinglePubkeyRecord, diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/multiple_zc.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/multiple_zc.rs index ad41a0419c..4d91230bc5 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/multiple_zc.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/multiple_zc.rs @@ -4,8 +4,7 @@ //! Verifies that the macro handles multiple AccountLoader fields correctly. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d11_zero_copy::ZcBasicRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_ata.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_ata.rs index 6b5188a965..08ccdb26a1 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_ata.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_ata.rs @@ -4,9 +4,9 @@ //! Verifies that zero-copy PDAs work alongside associated token account creation macros. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{ + CreateAccountsProof, LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR, +}; use crate::state::d11_zero_copy::ZcBasicRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_ctx_seeds.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_ctx_seeds.rs index ba3fdf4ea1..57cfc7c49d 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_ctx_seeds.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_ctx_seeds.rs @@ -4,8 +4,7 @@ //! Verifies that context account seeds work correctly with zero-copy accounts. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d11_zero_copy::ZcWithSeedsRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_mint_to.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_mint_to.rs index 1ddefed75a..329d18a1cd 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_mint_to.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_mint_to.rs @@ -4,9 +4,9 @@ //! Verifies that zero-copy PDAs work alongside token vault creation and MintTo CPI. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{ + CreateAccountsProof, LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR, +}; use crate::state::d11_zero_copy::ZcBasicRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_params_seeds.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_params_seeds.rs index cf0ffc1e86..feb1664fe8 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_params_seeds.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_params_seeds.rs @@ -4,8 +4,7 @@ //! Verifies that seed fields not present on the struct work correctly. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d11_zero_copy::ZcWithParamsRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_vault.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_vault.rs index 238b5c20e2..5572694738 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_vault.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d11_zero_copy/with_vault.rs @@ -4,9 +4,9 @@ //! Verifies that zero-copy PDAs work alongside token account creation macros. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{ + CreateAccountsProof, LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR, +}; use crate::state::d11_zero_copy::ZcBasicRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs index b80cb7b129..76ac801197 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs @@ -4,9 +4,9 @@ //! Note: #[light_account(init)] is tested separately in amm_test/initialize.rs. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{ + CreateAccountsProof, LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR, +}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/light_token.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/light_token.rs index bc01742f8a..74b3ed07bc 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/light_token.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/light_token.rs @@ -5,8 +5,7 @@ //! CreateTokenAccountCpi in the instruction handler. use anchor_lang::prelude::*; -use light_sdk_macros::LightAccounts; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; pub const D5_VAULT_AUTH_SEED: &[u8] = b"d5_vault_auth"; pub const D5_VAULT_SEED: &[u8] = b"d5_vault"; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_bare.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_bare.rs index 2b5be208df..af37c16fa0 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_bare.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_bare.rs @@ -7,8 +7,7 @@ //! because the RentFree derive macro generates code that accesses this field. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/account.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/account.rs index 313cf0db6e..ee11b60c9f 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/account.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/account.rs @@ -3,8 +3,7 @@ //! Tests that #[light_account(init)] works with Account<'info, T> directly (not boxed). use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs index 4eac80b3c9..a887cae86e 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs @@ -3,8 +3,7 @@ //! Tests that both account type variants work in the same struct. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::{ d1_field_types::single_pubkey::SinglePubkeyRecord, diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/boxed.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/boxed.rs index 5afbd7ebba..1e63c29ba4 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/boxed.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/boxed.rs @@ -4,8 +4,7 @@ //! This exercises the Box unwrap path in seed_extraction.rs with is_boxed = true. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs index ca99e0c695..c70573d5b0 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs @@ -3,9 +3,9 @@ //! Tests that different naming conventions work together in one struct. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{ + CreateAccountsProof, LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR, +}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/creator.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/creator.rs index e4807b49a9..3ca78cabdf 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/creator.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/creator.rs @@ -3,8 +3,7 @@ //! Tests that #[light_account(init)] works when the payer field is named `creator` instead of `fee_payer`. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/light_token_config.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/light_token_config.rs index 8595a13aff..34d5967667 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/light_token_config.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/light_token_config.rs @@ -5,8 +5,7 @@ //! but the token account is created manually via CreateTokenAccountCpi. use anchor_lang::prelude::*; -use light_sdk_macros::LightAccounts; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{LightAccounts, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; pub const D7_LIGHT_TOKEN_AUTH_SEED: &[u8] = b"d7_light_token_auth"; pub const D7_LIGHT_TOKEN_VAULT_SEED: &[u8] = b"d7_light_token_vault"; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/payer.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/payer.rs index a66c158810..4d69e3fe8f 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/payer.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/payer.rs @@ -3,8 +3,7 @@ //! Tests that #[light_account(init)] works when the payer field is named `payer` instead of `fee_payer`. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs index 7ea6e2e7b9..a6ad1e42ff 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs @@ -3,8 +3,7 @@ //! Tests the builder path with multiple #[light_account(init)] fields of different state types. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::{ d1_field_types::single_pubkey::SinglePubkeyRecord, diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/multi_rentfree.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/multi_rentfree.rs index 89e38cebcb..2bfedeceed 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/multi_rentfree.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/multi_rentfree.rs @@ -3,8 +3,7 @@ //! Tests the builder path with multiple #[light_account(init)] PDA accounts of the same type. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/pda_only.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/pda_only.rs index fbca73d371..719f1933c9 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/pda_only.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/pda_only.rs @@ -4,8 +4,7 @@ //! are marked with #[light_account(init)], without any token accounts. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/all.rs index bb2dbc9003..a1c6f09ee3 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/all.rs @@ -9,8 +9,7 @@ //! - FunctionCall: max_key(&a, &b) use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/array_bumps.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/array_bumps.rs index 7043588d1e..14013da0c0 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/array_bumps.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/array_bumps.rs @@ -5,8 +5,7 @@ //! not by including &[bump] in the seeds array. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/complex_mixed.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/complex_mixed.rs index 4171cc66a2..d51832c177 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/complex_mixed.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/complex_mixed.rs @@ -6,8 +6,7 @@ //! - Maximum seed complexity use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/const_patterns.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/const_patterns.rs index 69aef9712c..c65fee0031 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/const_patterns.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/const_patterns.rs @@ -7,8 +7,7 @@ //! - Trait associated constants: ::CONSTANT use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/constant.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/constant.rs index 24ba9848db..e0bf1b5ef4 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/constant.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/constant.rs @@ -3,8 +3,7 @@ //! Tests ClassifiedSeed::Constant with constant identifier seeds. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/ctx_account.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/ctx_account.rs index 3fe3258a49..0fc2533ffc 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/ctx_account.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/ctx_account.rs @@ -3,8 +3,7 @@ //! Tests ClassifiedSeed::CtxAccount with authority.key() seeds. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/edge_cases.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/edge_cases.rs index 53640c3062..0de5037c2e 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/edge_cases.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/edge_cases.rs @@ -9,8 +9,7 @@ //! - Many literals in same seeds array use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/external_paths.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/external_paths.rs index c06c509f31..7877e63f43 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/external_paths.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/external_paths.rs @@ -6,8 +6,7 @@ //! - Complex nested external paths use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; @@ -21,7 +20,7 @@ pub struct D9ExternalSdkTypesParams { pub owner: Pubkey, } -/// Tests external crate path: light_sdk_types::constants::CPI_AUTHORITY_PDA_SEED +/// Tests external crate path: light_account::constants::CPI_AUTHORITY_PDA_SEED #[derive(Accounts, LightAccounts)] #[instruction(params: D9ExternalSdkTypesParams)] pub struct D9ExternalSdkTypes<'info> { @@ -39,7 +38,7 @@ pub struct D9ExternalSdkTypes<'info> { init, payer = fee_payer, space = 8 + SinglePubkeyRecord::INIT_SPACE, - seeds = [b"d9_ext_sdk", light_sdk_types::constants::CPI_AUTHORITY_PDA_SEED, params.owner.as_ref()], + seeds = [b"d9_ext_sdk", light_account::constants::CPI_AUTHORITY_PDA_SEED, params.owner.as_ref()], bump, )] #[light_account(init)] @@ -114,7 +113,7 @@ pub struct D9ExternalMixed<'info> { payer = fee_payer, space = 8 + SinglePubkeyRecord::INIT_SPACE, seeds = [ - light_sdk_types::constants::CPI_AUTHORITY_PDA_SEED, + light_account::constants::CPI_AUTHORITY_PDA_SEED, light_token_types::constants::POOL_SEED, params.owner.as_ref() ], @@ -157,7 +156,7 @@ pub struct D9ExternalWithLocal<'info> { init, payer = fee_payer, space = 8 + SinglePubkeyRecord::INIT_SPACE, - seeds = [D9_EXTERNAL_LOCAL, light_sdk_types::constants::RENT_SPONSOR_SEED, params.owner.as_ref()], + seeds = [D9_EXTERNAL_LOCAL, light_account::constants::RENT_SPONSOR_SEED, params.owner.as_ref()], bump, )] #[light_account(init)] @@ -208,7 +207,7 @@ pub struct D9ExternalBump<'info> { // ============================================================================ /// Re-export from external crate for path testing -pub use light_sdk_types::constants::CPI_AUTHORITY_PDA_SEED as REEXPORTED_SEED; +pub use light_account::CPI_AUTHORITY_PDA_SEED as REEXPORTED_SEED; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] pub struct D9ExternalReexportParams { diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/function_call.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/function_call.rs index f594857d37..de81bbce37 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/function_call.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/function_call.rs @@ -3,8 +3,7 @@ //! Tests ClassifiedSeed::FunctionCall with max_key(&a, &b) seeds. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/instruction_data.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/instruction_data.rs index 398e3f00d7..bf02b87a3b 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/instruction_data.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/instruction_data.rs @@ -9,8 +9,7 @@ //! so we test naming variations within the seed expressions, not the param struct name. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/literal.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/literal.rs index d6f7909a49..4daaf1b2d6 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/literal.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/literal.rs @@ -3,8 +3,7 @@ //! Tests ClassifiedSeed::Literal with byte literal seeds. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/method_chains.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/method_chains.rs index 7ad91d6d4a..af4aa4b277 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/method_chains.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/method_chains.rs @@ -8,8 +8,7 @@ //! - Method chains on qualified paths use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mixed.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mixed.rs index d58a845169..b499a8eaa8 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mixed.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mixed.rs @@ -3,8 +3,7 @@ //! Tests multiple seed types combined: literal + ctx_account + param. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/nested_seeds.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/nested_seeds.rs index 4e2ebd7db8..a42e41e36b 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/nested_seeds.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/nested_seeds.rs @@ -6,8 +6,7 @@ //! - Complex nested struct paths use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param.rs index 1acdbf0555..562c1eacfe 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param.rs @@ -3,8 +3,7 @@ //! Tests ClassifiedSeed::DataField with params.owner.as_ref() seeds. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param_bytes.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param_bytes.rs index 9295c55d6f..3720584355 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param_bytes.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param_bytes.rs @@ -3,8 +3,7 @@ //! Tests ClassifiedSeed::DataField with params.id.to_le_bytes() conversion. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/qualified_paths.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/qualified_paths.rs index c6616fd4a8..f15d1e830e 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/qualified_paths.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/qualified_paths.rs @@ -7,8 +7,7 @@ //! - Nested module paths use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; 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 f4014cb8a1..a663bf7520 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -2,10 +2,10 @@ #![allow(clippy::useless_asref)] // Testing macro handling of .as_ref() patterns use anchor_lang::prelude::*; +use light_account::{ + derive_light_cpi_signer, derive_light_rent_sponsor_pda, light_program, CpiSigner, +}; use light_instruction_decoder_derive::instruction_decoder; -use light_sdk::{derive_light_cpi_signer, derive_light_rent_sponsor_pda}; -use light_sdk_macros::light_program; -use light_sdk_types::CpiSigner; pub mod amm_test; pub mod d5_markers; @@ -314,9 +314,8 @@ pub mod csdk_anchor_full_derived_test { ctx: Context<'_, '_, '_, 'info, CreatePdasAndMintAuto<'info>>, params: FullAutoWithMintParams, ) -> Result<()> { - use light_token::instruction::{ - CreateTokenAccountCpi, CreateTokenAtaCpi, MintToCpi as CTokenMintToCpi, - }; + use light_account::{CreateTokenAccountCpi, CreateTokenAtaCpi}; + use light_token::instruction::MintToCpi as CTokenMintToCpi; let user_record = &mut ctx.accounts.user_record; user_record.owner = params.owner; @@ -333,38 +332,51 @@ pub mod csdk_anchor_full_derived_test { game_session.score = 0; // vault is mark-only - create manually via CreateTokenAccountCpi - CreateTokenAccountCpi { - payer: ctx.accounts.fee_payer.to_account_info(), - account: ctx.accounts.vault.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - owner: ctx.accounts.vault_authority.key(), + { + let payer_info = ctx.accounts.fee_payer.to_account_info(); + let account_info = ctx.accounts.vault.to_account_info(); + let mint_info = ctx.accounts.mint.to_account_info(); + let config_info = ctx.accounts.light_token_config.to_account_info(); + let sponsor_info = ctx.accounts.light_token_rent_sponsor.to_account_info(); + let system_info = ctx.accounts.system_program.to_account_info(); + CreateTokenAccountCpi { + payer: &payer_info, + account: &account_info, + mint: &mint_info, + owner: ctx.accounts.vault_authority.key().to_bytes(), + } + .rent_free( + &config_info, + &sponsor_info, + &system_info, + &crate::ID.to_bytes(), + ) + .invoke_signed(&[ + VAULT_SEED, + ctx.accounts.mint.to_account_info().key.as_ref(), + &[ctx.bumps.vault], + ])?; } - .rent_free( - ctx.accounts.light_token_config.to_account_info(), - ctx.accounts.light_token_rent_sponsor.to_account_info(), - ctx.accounts.system_program.to_account_info(), - &crate::ID, - ) - .invoke_signed(&[ - VAULT_SEED, - ctx.accounts.mint.to_account_info().key.as_ref(), - &[ctx.bumps.vault], - ])?; - CreateTokenAtaCpi { - payer: ctx.accounts.fee_payer.to_account_info(), - owner: ctx.accounts.fee_payer.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - ata: ctx.accounts.user_ata.to_account_info(), - bump: params.user_ata_bump, + { + let payer_info = ctx.accounts.fee_payer.to_account_info(); + let owner_info = ctx.accounts.fee_payer.to_account_info(); + let mint_info = ctx.accounts.mint.to_account_info(); + let ata_info = ctx.accounts.user_ata.to_account_info(); + let config_info = ctx.accounts.light_token_config.to_account_info(); + let sponsor_info = ctx.accounts.light_token_rent_sponsor.to_account_info(); + let system_info = ctx.accounts.system_program.to_account_info(); + CreateTokenAtaCpi { + payer: &payer_info, + owner: &owner_info, + mint: &mint_info, + ata: &ata_info, + bump: params.user_ata_bump, + } + .idempotent() + .rent_free(&config_info, &sponsor_info, &system_info) + .invoke()?; } - .idempotent() - .rent_free( - ctx.accounts.light_token_config.to_account_info(), - ctx.accounts.light_token_rent_sponsor.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ) - .invoke()?; if params.vault_mint_amount > 0 { CTokenMintToCpi { @@ -614,21 +626,22 @@ pub mod csdk_anchor_full_derived_test { ctx: Context<'_, '_, '_, 'info, D7LightTokenConfig<'info>>, params: D7LightTokenConfigParams, ) -> Result<()> { - use light_token::instruction::CreateTokenAccountCpi; + use light_account::CreateTokenAccountCpi; // Token vault is mark-only - create manually via CreateTokenAccountCpi + let __payer = ctx.accounts.fee_payer.to_account_info(); + let __account = ctx.accounts.d7_light_token_vault.to_account_info(); + let __mint = ctx.accounts.mint.to_account_info(); + let __config = ctx.accounts.light_token_config.to_account_info(); + let __sponsor = ctx.accounts.light_token_rent_sponsor.to_account_info(); + let __sys = ctx.accounts.system_program.to_account_info(); CreateTokenAccountCpi { - payer: ctx.accounts.fee_payer.to_account_info(), - account: ctx.accounts.d7_light_token_vault.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - owner: ctx.accounts.d7_light_token_authority.key(), + payer: &__payer, + account: &__account, + mint: &__mint, + owner: ctx.accounts.d7_light_token_authority.key().to_bytes(), } - .rent_free( - ctx.accounts.light_token_config.to_account_info(), - ctx.accounts.light_token_rent_sponsor.to_account_info(), - ctx.accounts.system_program.to_account_info(), - &crate::ID, - ) + .rent_free(&__config, &__sponsor, &__sys, &crate::ID.to_bytes()) .invoke_signed(&[ D7_LIGHT_TOKEN_VAULT_SEED, ctx.accounts.mint.to_account_info().key.as_ref(), @@ -643,24 +656,25 @@ pub mod csdk_anchor_full_derived_test { ctx: Context<'_, '_, '_, 'info, D7AllNames<'info>>, params: D7AllNamesParams, ) -> Result<()> { - use light_token::instruction::CreateTokenAccountCpi; + use light_account::CreateTokenAccountCpi; // Set up the PDA record ctx.accounts.d7_all_record.owner = params.owner; // Token vault is mark-only - create manually via CreateTokenAccountCpi + let __payer = ctx.accounts.payer.to_account_info(); + let __account = ctx.accounts.d7_all_vault.to_account_info(); + let __mint = ctx.accounts.mint.to_account_info(); + let __config = ctx.accounts.light_token_config.to_account_info(); + let __sponsor = ctx.accounts.light_token_rent_sponsor.to_account_info(); + let __sys = ctx.accounts.system_program.to_account_info(); CreateTokenAccountCpi { - payer: ctx.accounts.payer.to_account_info(), - account: ctx.accounts.d7_all_vault.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - owner: ctx.accounts.d7_all_authority.key(), + payer: &__payer, + account: &__account, + mint: &__mint, + owner: ctx.accounts.d7_all_authority.key().to_bytes(), } - .rent_free( - ctx.accounts.light_token_config.to_account_info(), - ctx.accounts.light_token_rent_sponsor.to_account_info(), - ctx.accounts.system_program.to_account_info(), - &crate::ID, - ) + .rent_free(&__config, &__sponsor, &__sys, &crate::ID.to_bytes()) .invoke_signed(&[ D7_ALL_VAULT_SEED, ctx.accounts.mint.to_account_info().key.as_ref(), @@ -1336,21 +1350,22 @@ pub mod csdk_anchor_full_derived_test { ctx: Context<'_, '_, '_, 'info, D5LightToken<'info>>, params: D5LightTokenParams, ) -> Result<()> { - use light_token::instruction::CreateTokenAccountCpi; + use light_account::CreateTokenAccountCpi; // Token vault is mark-only - create manually via CreateTokenAccountCpi + let __payer = ctx.accounts.fee_payer.to_account_info(); + let __account = ctx.accounts.d5_token_vault.to_account_info(); + let __mint = ctx.accounts.mint.to_account_info(); + let __config = ctx.accounts.light_token_config.to_account_info(); + let __sponsor = ctx.accounts.light_token_rent_sponsor.to_account_info(); + let __sys = ctx.accounts.system_program.to_account_info(); CreateTokenAccountCpi { - payer: ctx.accounts.fee_payer.to_account_info(), - account: ctx.accounts.d5_token_vault.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - owner: ctx.accounts.vault_authority.key(), + payer: &__payer, + account: &__account, + mint: &__mint, + owner: ctx.accounts.vault_authority.key().to_bytes(), } - .rent_free( - ctx.accounts.light_token_config.to_account_info(), - ctx.accounts.light_token_rent_sponsor.to_account_info(), - ctx.accounts.system_program.to_account_info(), - &crate::ID, - ) + .rent_free(&__config, &__sponsor, &__sys, &crate::ID.to_bytes()) .invoke_signed(&[ D5_VAULT_SEED, ctx.accounts.mint.to_account_info().key.as_ref(), @@ -1365,24 +1380,25 @@ pub mod csdk_anchor_full_derived_test { ctx: Context<'_, '_, '_, 'info, D5AllMarkers<'info>>, params: D5AllMarkersParams, ) -> Result<()> { - use light_token::instruction::CreateTokenAccountCpi; + use light_account::CreateTokenAccountCpi; // Set up the PDA record ctx.accounts.d5_all_record.owner = params.owner; // Token vault is mark-only - create manually via CreateTokenAccountCpi + let __payer = ctx.accounts.fee_payer.to_account_info(); + let __account = ctx.accounts.d5_all_vault.to_account_info(); + let __mint = ctx.accounts.mint.to_account_info(); + let __config = ctx.accounts.light_token_config.to_account_info(); + let __sponsor = ctx.accounts.light_token_rent_sponsor.to_account_info(); + let __sys = ctx.accounts.system_program.to_account_info(); CreateTokenAccountCpi { - payer: ctx.accounts.fee_payer.to_account_info(), - account: ctx.accounts.d5_all_vault.to_account_info(), - mint: ctx.accounts.mint.to_account_info(), - owner: ctx.accounts.d5_all_authority.key(), + payer: &__payer, + account: &__account, + mint: &__mint, + owner: ctx.accounts.d5_all_authority.key().to_bytes(), } - .rent_free( - ctx.accounts.light_token_config.to_account_info(), - ctx.accounts.light_token_rent_sponsor.to_account_info(), - ctx.accounts.system_program.to_account_info(), - &crate::ID, - ) + .rent_free(&__config, &__sponsor, &__sys, &crate::ID.to_bytes()) .invoke_signed(&[ D5_ALL_VAULT_SEED, ctx.accounts.mint.to_account_info().key.as_ref(), @@ -1433,22 +1449,25 @@ pub mod csdk_anchor_full_derived_test { ctx: Context<'_, '_, '_, 'info, D10SingleAtaMarkonly<'info>>, params: D10SingleAtaMarkonlyParams, ) -> Result<()> { - use light_token::instruction::CreateTokenAtaCpi; + use light_account::CreateTokenAtaCpi; // Mark-only: LightPreInit/LightFinalize are no-ops, we create the ATA manually + let __payer = ctx.accounts.fee_payer.to_account_info(); + let __owner = ctx.accounts.d10_markonly_ata_owner.to_account_info(); + let __mint = ctx.accounts.d10_markonly_ata_mint.to_account_info(); + let __ata = ctx.accounts.d10_markonly_ata.to_account_info(); + let __config = ctx.accounts.light_token_config.to_account_info(); + let __sponsor = ctx.accounts.light_token_rent_sponsor.to_account_info(); + let __sys = ctx.accounts.system_program.to_account_info(); CreateTokenAtaCpi { - payer: ctx.accounts.fee_payer.to_account_info(), - owner: ctx.accounts.d10_markonly_ata_owner.to_account_info(), - mint: ctx.accounts.d10_markonly_ata_mint.to_account_info(), - ata: ctx.accounts.d10_markonly_ata.to_account_info(), + payer: &__payer, + owner: &__owner, + mint: &__mint, + ata: &__ata, bump: params.ata_bump, } .idempotent() - .rent_free( - ctx.accounts.light_token_config.to_account_info(), - ctx.accounts.light_token_rent_sponsor.to_account_info(), - ctx.accounts.system_program.to_account_info(), - ) + .rent_free(&__config, &__sponsor, &__sys) .invoke()?; Ok(()) diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/basic.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/basic.rs index 7d8d23a339..bdee8d782c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/basic.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/basic.rs @@ -3,8 +3,7 @@ //! Tests `#[light_account(init, zero_copy)]` with a simple Pod account. use anchor_lang::prelude::*; -use light_sdk::{interface::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// Basic zero-copy record for simple tests (no Pubkey seeds on the struct itself). /// Used with AccountLoader<'info, ZcBasicRecord>. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/with_params.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/with_params.rs index 5e2c4ce710..1f6da63bab 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/with_params.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/with_params.rs @@ -3,8 +3,7 @@ //! Tests `#[light_account(init, zero_copy)]` with params-only seeds (not on struct). use anchor_lang::prelude::*; -use light_sdk::{interface::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// Zero-copy record for testing params-only seeds (category_id in seeds but not on struct). /// The PDA seeds may include params.category_id which is not stored on this struct. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/with_seeds.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/with_seeds.rs index 878241dac9..6d8fc0cabd 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/with_seeds.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d11_zero_copy/with_seeds.rs @@ -3,8 +3,7 @@ //! Tests `#[light_account(init, zero_copy)]` with context account seeds. use anchor_lang::prelude::*; -use light_sdk::{interface::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// Zero-copy record with authority field for testing ctx.accounts.* seed packing. /// The authority field will be used in PDA seeds derived from ctx.accounts.authority. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/all.rs index 0a31b380c6..87f77f4ce5 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/all.rs @@ -8,8 +8,7 @@ //! - Option (-> unchanged) use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// Comprehensive struct with all field type variations. #[derive(Default, Debug, InitSpace, LightAccount)] diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/arrays.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/arrays.rs index 3c081d9ff1..c51ae73118 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/arrays.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/arrays.rs @@ -3,8 +3,7 @@ //! Exercises the code path for array field handling. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct with array fields. /// Tests [u8; 32] (byte array) and fixed-size arrays. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/multi_pubkey.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/multi_pubkey.rs index d28b4f5ca1..549aa6689a 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/multi_pubkey.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/multi_pubkey.rs @@ -4,8 +4,7 @@ //! generating a PackedX struct with multiple u8 index fields. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct with multiple Pubkey fields. /// PackedMultiPubkeyRecord will have: owner_index, delegate_index, authority_index: u8 diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/no_pubkey.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/no_pubkey.rs index 3edfd57fee..7bbf92b7e9 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/no_pubkey.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/no_pubkey.rs @@ -4,8 +4,7 @@ //! resulting in Pack/Unpack being a type alias (identity). use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct with only primitive fields - no Pubkey. /// This tests the identity Pack path where PackedNoPubkeyRecord = NoPubkeyRecord. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/non_copy.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/non_copy.rs index c0b566dfae..577d941bdb 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/non_copy.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/non_copy.rs @@ -4,8 +4,7 @@ //! which triggers the `.clone()` path in pack/unpack generation. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct with a String field (non-Copy type). /// This tests the clone() code path for non-Copy fields. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_primitive.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_primitive.rs index c083be5e23..c1b73a2720 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_primitive.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_primitive.rs @@ -4,8 +4,7 @@ //! These remain unchanged in the packed struct (not converted to u8 index). use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct with Option fields. /// These stay as Option in the packed struct (not Option). diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_pubkey.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_pubkey.rs index e9411786b6..0980dfcc2c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_pubkey.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_pubkey.rs @@ -4,8 +4,7 @@ //! which generates Option in the packed struct. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct with Option fields. /// PackedOptionPubkeyRecord will have: delegate_index: Option diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/single_pubkey.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/single_pubkey.rs index c3cbb29ae2..0715ff510a 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/single_pubkey.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/single_pubkey.rs @@ -4,8 +4,7 @@ //! generating a PackedX struct with a single u8 index field. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct with exactly one Pubkey field. /// PackedSinglePubkeyRecord will have: owner_index: u8 diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/absent.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/absent.rs index cfebaa127e..bdcc6675ec 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/absent.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/absent.rs @@ -4,8 +4,7 @@ //! All fields use self.field directly for compression. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct without any compress_as attribute. /// All fields are compressed as-is using self.field. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/all.rs index d9ce8eefaf..30524320ae 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/all.rs @@ -6,8 +6,7 @@ //! - Fields without override (use self.field) use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// Comprehensive struct with all compress_as variations. #[derive(Default, Debug, InitSpace, LightAccount)] diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/multiple.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/multiple.rs index aff8279b6a..3d24ae903a 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/multiple.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/multiple.rs @@ -3,8 +3,7 @@ //! Exercises the code path where multiple fields have compress_as overrides. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct with multiple compress_as overrides. /// start, score, and cached all have compression overrides. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/option_none.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/option_none.rs index 6d4f937f0f..a435569273 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/option_none.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/option_none.rs @@ -3,8 +3,7 @@ //! Exercises the code path where Option fields are compressed as None. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct with compress_as None for Option fields. /// end_time is compressed as None instead of self.end_time. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/single.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/single.rs index 1ab1515848..524318bd9d 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/single.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/single.rs @@ -3,8 +3,7 @@ //! Exercises the code path where one field has a compress_as override. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A struct with single compress_as override. /// cached field is compressed as 0 instead of self.cached. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/all.rs index a278d75280..3e7c1bd9de 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/all.rs @@ -3,8 +3,7 @@ //! Exercises a large struct with all field type variants from D1. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// Comprehensive large struct with all field types. /// 15+ fields to trigger SHA256 mode with all D1 variations. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/info_last.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/info_last.rs index ec85183613..314d22c03b 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/info_last.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/info_last.rs @@ -3,8 +3,7 @@ //! Exercises struct validation with compression_info in non-first position. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// Struct with compression_info as last field. /// Tests that field ordering is handled correctly. diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/large.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/large.rs index 0b3308e699..7287142b4f 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/large.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/large.rs @@ -3,8 +3,7 @@ //! Exercises the hash mode selection for large structs (SHA256 path). use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// Large struct with 12+ fields for SHA256 hash mode. #[derive(Default, Debug, InitSpace, LightAccount)] diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/minimal.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/minimal.rs index 3531b50652..f9871abc76 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/minimal.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/minimal.rs @@ -3,8 +3,7 @@ //! Exercises the smallest valid struct with compression_info and one field. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// Smallest valid struct: compression_info + one field. #[derive(Default, Debug, InitSpace, LightAccount)] diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs index fe2bd3eb4b..5dbc5fd872 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs @@ -1,11 +1,9 @@ //! State structs for the test program and test cases organized by dimension. use anchor_lang::prelude::*; -use light_sdk::{ - compressible::CompressionInfo, instruction::PackedAddressTreeInfo, LightDiscriminator, +use light_account::{ + CompressionInfo, LightAccount, LightDiscriminator, PackedAddressTreeInfo, ValidityProof, }; -use light_sdk_macros::LightAccount; -use light_token::ValidityProof; use light_token_interface::instructions::mint_action::MintWithContext; // Test modules diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs index 60862565c0..dbfa05155c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs @@ -10,11 +10,9 @@ //! testing Pack/Unpack behavior with array fields and nested data structures. use csdk_anchor_full_derived_test::{Observation, ObservationState, PackedObservationState}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -279,7 +277,7 @@ fn test_pack_converts_pool_id_to_index() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 1); - assert_eq!(stored_pubkeys[0], pool_id); + assert_eq!(stored_pubkeys[0], pool_id.to_bytes()); } #[test] @@ -495,7 +493,8 @@ fn test_pack_stores_pool_id_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 1, "should have 1 pubkey stored"); assert_eq!( - stored_pubkeys[packed.pool_id as usize], pool_id, + stored_pubkeys[packed.pool_id as usize], + pool_id.to_bytes(), "stored pubkey should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs index f8a47832fa..4b8ad2c758 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs @@ -10,11 +10,9 @@ //! comprehensive Pack/Unpack behavior with multiple pubkey indices. use csdk_anchor_full_derived_test::{PackedPoolState, PoolState}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -311,7 +309,7 @@ fn test_pack_converts_all_10_pubkeys_to_indices() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 10); for (i, pubkey) in pubkeys.iter().enumerate() { - assert_eq!(stored_pubkeys[i], *pubkey); + assert_eq!(stored_pubkeys[i], pubkey.to_bytes()); } } @@ -522,7 +520,8 @@ fn test_pack_stores_all_pubkeys_in_packed_accounts() { // Verify each pubkey is stored at its index for (i, expected_pubkey) in pubkeys.iter().enumerate() { assert_eq!( - stored_pubkeys[i], *expected_pubkey, + stored_pubkeys[i], + expected_pubkey.to_bytes(), "pubkey at index {} should match", i ); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs index 0d4d4d781e..4bd7e13273 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs @@ -10,11 +10,9 @@ //! which overrides field values during compression. use csdk_anchor_full_derived_test::{GameSession, PackedGameSession}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -442,11 +440,13 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); assert_eq!( - stored_pubkeys[packed1.player as usize], player1, + stored_pubkeys[packed1.player as usize], + player1.to_bytes(), "first pubkey should match" ); assert_eq!( - stored_pubkeys[packed2.player as usize], player2, + stored_pubkeys[packed2.player as usize], + player2.to_bytes(), "second pubkey should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs index f73166ec02..90af079cd8 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs @@ -7,11 +7,9 @@ //! - CompressiblePack -> Pack + Unpack + PackedPlaceholderRecord use csdk_anchor_full_derived_test::{PackedPlaceholderRecord, PlaceholderRecord}; +use light_account::{CompressAs, CompressionInfo, CompressionState, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, CompressionState, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -355,11 +353,13 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); assert_eq!( - stored_pubkeys[packed1.owner as usize], owner1, + stored_pubkeys[packed1.owner as usize], + owner1.to_bytes(), "first pubkey should match" ); assert_eq!( - stored_pubkeys[packed2.owner as usize], owner2, + stored_pubkeys[packed2.owner as usize], + owner2.to_bytes(), "second pubkey should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs index 507be688ae..5cb0699a55 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs @@ -6,11 +6,9 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace use csdk_anchor_full_derived_test::{PackedUserRecord, UserRecord}; +use light_account::{CompressAs, CompressionInfo, CompressionState, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, CompressionState, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -354,11 +352,13 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); assert_eq!( - stored_pubkeys[packed1.owner as usize], owner1, + stored_pubkeys[packed1.owner as usize], + owner1.to_bytes(), "first pubkey should match" ); assert_eq!( - stored_pubkeys[packed2.owner as usize], owner2, + stored_pubkeys[packed2.owner as usize], + owner2.to_bytes(), "second pubkey should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs index f794e963b1..9819b70507 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs @@ -14,11 +14,9 @@ //! - Regular primitives (counter, flag) -> direct copy use csdk_anchor_full_derived_test::{AllFieldTypesRecord, PackedAllFieldTypesRecord}; +use light_account::{CompressAs, CompressionInfo, CompressionState, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, CompressionState, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -471,9 +469,9 @@ fn test_pack_converts_all_pubkey_types() { // Only direct Pubkey fields are stored in packed_accounts (not Option) let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 3); - assert_eq!(stored_pubkeys[0], owner); - assert_eq!(stored_pubkeys[1], delegate); - assert_eq!(stored_pubkeys[2], authority); + assert_eq!(stored_pubkeys[0], owner.to_bytes()); + assert_eq!(stored_pubkeys[1], delegate.to_bytes()); + assert_eq!(stored_pubkeys[2], authority.to_bytes()); } #[test] diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs index d97239f8d3..a3825b9c06 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs @@ -10,11 +10,8 @@ //! Therefore, no Pack/Unpack tests are needed. use csdk_anchor_full_derived_test::ArrayRecord; +use light_account::{CompressAs, CompressionInfo, CompressionState}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::CompressionState, - interface::{CompressAs, CompressionInfo}, -}; use super::shared::CompressibleTestFactory; use crate::generate_trait_tests; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs index 8b3bcf7e89..12e31c740e 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs @@ -6,11 +6,9 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace use csdk_anchor_full_derived_test::{MultiPubkeyRecord, PackedMultiPubkeyRecord}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -94,7 +92,7 @@ fn test_compress_as_when_compression_info_already_none() { // Should still work and preserve fields assert_eq!( compressed.compression_info.state, - light_sdk::compressible::CompressionState::Compressed + light_account::CompressionState::Compressed ); assert_eq!(compressed.owner, owner); assert_eq!(compressed.delegate, delegate); @@ -273,9 +271,9 @@ fn test_pack_converts_all_pubkeys_to_indices() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 3); - assert_eq!(stored_pubkeys[0], owner); - assert_eq!(stored_pubkeys[1], delegate); - assert_eq!(stored_pubkeys[2], authority); + assert_eq!(stored_pubkeys[0], owner.to_bytes()); + assert_eq!(stored_pubkeys[1], delegate.to_bytes()); + assert_eq!(stored_pubkeys[2], authority.to_bytes()); } #[test] @@ -385,27 +383,33 @@ fn test_pack_stores_all_pubkeys_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 6, "should have 6 pubkeys stored"); assert_eq!( - stored_pubkeys[packed1.owner as usize], owner1, + stored_pubkeys[packed1.owner as usize], + owner1.to_bytes(), "first record owner should match" ); assert_eq!( - stored_pubkeys[packed1.delegate as usize], delegate1, + stored_pubkeys[packed1.delegate as usize], + delegate1.to_bytes(), "first record delegate should match" ); assert_eq!( - stored_pubkeys[packed1.authority as usize], authority1, + stored_pubkeys[packed1.authority as usize], + authority1.to_bytes(), "first record authority should match" ); assert_eq!( - stored_pubkeys[packed2.owner as usize], owner2, + stored_pubkeys[packed2.owner as usize], + owner2.to_bytes(), "second record owner should match" ); assert_eq!( - stored_pubkeys[packed2.delegate as usize], delegate2, + stored_pubkeys[packed2.delegate as usize], + delegate2.to_bytes(), "second record delegate should match" ); assert_eq!( - stored_pubkeys[packed2.authority as usize], authority2, + stored_pubkeys[packed2.authority as usize], + authority2.to_bytes(), "second record authority should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs index 2b10e3f05d..da62b3ea78 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs @@ -10,11 +10,8 @@ //! struct is packed as-is without transformation. use csdk_anchor_full_derived_test::NoPubkeyRecord; +use light_account::{CompressAs, CompressionInfo, CompressionState}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::CompressionState, - interface::{CompressAs, CompressionInfo}, -}; use super::shared::CompressibleTestFactory; use crate::generate_trait_tests; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs index 7d2c4cf8e1..291a65662b 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs @@ -10,8 +10,8 @@ //! Therefore, no Pack/Unpack tests are needed. use csdk_anchor_full_derived_test::NonCopyRecord; +use light_account::{CompressAs, CompressionInfo}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::interface::{CompressAs, CompressionInfo}; use super::shared::CompressibleTestFactory; use crate::generate_trait_tests; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs index a12e6dd709..624f724663 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs @@ -10,8 +10,8 @@ //! struct (not converted to Option). Therefore, no Pack/Unpack tests are needed. use csdk_anchor_full_derived_test::OptionPrimitiveRecord; +use light_account::{CompressAs, CompressionInfo}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::interface::{CompressAs, CompressionInfo}; use super::shared::CompressibleTestFactory; use crate::generate_trait_tests; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs index d0922e2ea9..d4d6a9856c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs @@ -10,11 +10,9 @@ //! Option fields remain as Option in the packed struct. use csdk_anchor_full_derived_test::OptionPubkeyRecord; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -268,7 +266,7 @@ fn test_pack_converts_pubkey_to_index() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 1); - assert_eq!(stored_pubkeys[0], owner); + assert_eq!(stored_pubkeys[0], owner.to_bytes()); } #[test] @@ -298,7 +296,7 @@ fn test_pack_preserves_option_pubkey_as_option_pubkey() { // Only the direct Pubkey field (owner) is stored in packed_accounts let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 1); - assert_eq!(stored_pubkeys[0], owner); + assert_eq!(stored_pubkeys[0], owner.to_bytes()); } #[test] @@ -331,7 +329,7 @@ fn test_pack_option_pubkey_none_stays_none() { // Only the direct Pubkey field (owner) is stored in packed_accounts let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 1); - assert_eq!(stored_pubkeys[0], owner); + assert_eq!(stored_pubkeys[0], owner.to_bytes()); } #[test] @@ -361,7 +359,7 @@ fn test_pack_all_option_pubkeys_some() { // Only the direct Pubkey field (owner) is stored in packed_accounts let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 1); - assert_eq!(stored_pubkeys[0], owner); + assert_eq!(stored_pubkeys[0], owner.to_bytes()); } #[test] @@ -386,7 +384,7 @@ fn test_pack_all_option_pubkeys_none() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 1); - assert_eq!(stored_pubkeys[0], owner); + assert_eq!(stored_pubkeys[0], owner.to_bytes()); } #[test] diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs index f8666e8c22..0eb5736452 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs @@ -6,11 +6,9 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace use csdk_anchor_full_derived_test::{PackedSinglePubkeyRecord, SinglePubkeyRecord}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -80,7 +78,7 @@ fn test_compress_as_when_compression_info_already_none() { // Should still work and preserve fields assert_eq!( compressed.compression_info.state, - light_sdk::compressible::CompressionState::Compressed + light_account::CompressionState::Compressed ); assert_eq!(compressed.owner, owner); assert_eq!(compressed.counter, counter); @@ -260,11 +258,13 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); assert_eq!( - stored_pubkeys[packed1.owner as usize], owner1, + stored_pubkeys[packed1.owner as usize], + owner1.to_bytes(), "first pubkey should match" ); assert_eq!( - stored_pubkeys[packed2.owner as usize], owner2, + stored_pubkeys[packed2.owner as usize], + owner2.to_bytes(), "second pubkey should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs index 35202490dd..50408387cf 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs @@ -6,11 +6,9 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace use csdk_anchor_full_derived_test::{AllCompressAsRecord, PackedAllCompressAsRecord}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -461,11 +459,13 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); assert_eq!( - stored_pubkeys[packed1.owner as usize], owner1, + stored_pubkeys[packed1.owner as usize], + owner1.to_bytes(), "first pubkey should match" ); assert_eq!( - stored_pubkeys[packed2.owner as usize], owner2, + stored_pubkeys[packed2.owner as usize], + owner2.to_bytes(), "second pubkey should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs index 23287b6e13..091e703198 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs @@ -6,11 +6,9 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace use csdk_anchor_full_derived_test::{MultipleCompressAsRecord, PackedMultipleCompressAsRecord}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -385,11 +383,13 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); assert_eq!( - stored_pubkeys[packed1.owner as usize], owner1, + stored_pubkeys[packed1.owner as usize], + owner1.to_bytes(), "first pubkey should match" ); assert_eq!( - stored_pubkeys[packed2.owner as usize], owner2, + stored_pubkeys[packed2.owner as usize], + owner2.to_bytes(), "second pubkey should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs index 4bb6b08cc9..fe8ed06484 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs @@ -6,11 +6,9 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace use csdk_anchor_full_derived_test::{NoCompressAsRecord, PackedNoCompressAsRecord}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -305,11 +303,13 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); assert_eq!( - stored_pubkeys[packed1.owner as usize], owner1, + stored_pubkeys[packed1.owner as usize], + owner1.to_bytes(), "first pubkey should match" ); assert_eq!( - stored_pubkeys[packed2.owner as usize], owner2, + stored_pubkeys[packed2.owner as usize], + owner2.to_bytes(), "second pubkey should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs index 3a96760a18..013367c091 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs @@ -6,11 +6,9 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace use csdk_anchor_full_derived_test::{OptionNoneCompressAsRecord, PackedOptionNoneCompressAsRecord}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -390,11 +388,13 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); assert_eq!( - stored_pubkeys[packed1.owner as usize], owner1, + stored_pubkeys[packed1.owner as usize], + owner1.to_bytes(), "first pubkey should match" ); assert_eq!( - stored_pubkeys[packed2.owner as usize], owner2, + stored_pubkeys[packed2.owner as usize], + owner2.to_bytes(), "second pubkey should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs index f757852740..7c5f250044 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs @@ -7,11 +7,9 @@ //! - CompressiblePack -> Pack + Unpack + PackedSingleCompressAsRecord use csdk_anchor_full_derived_test::{PackedSingleCompressAsRecord, SingleCompressAsRecord}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; @@ -307,11 +305,13 @@ fn test_pack_stores_pubkeys_in_packed_accounts() { let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); assert_eq!( - stored_pubkeys[packed1.owner as usize], owner1, + stored_pubkeys[packed1.owner as usize], + owner1.to_bytes(), "first pubkey should match" ); assert_eq!( - stored_pubkeys[packed2.owner as usize], owner2, + stored_pubkeys[packed2.owner as usize], + owner2.to_bytes(), "second pubkey should match" ); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs index d83071f494..1100a0f958 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs @@ -10,11 +10,9 @@ //! This tests full Pack/Unpack behavior with compress_as attribute overrides. use csdk_anchor_full_derived_test::{AllCompositionRecord, PackedAllCompositionRecord}; +use light_account::{CompressAs, CompressionInfo, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs index e122ef70c1..38c81554f2 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs @@ -9,11 +9,9 @@ //! compression_info can be placed in non-first position (ordering test). use csdk_anchor_full_derived_test::{InfoLastRecord, PackedInfoLastRecord}; +use light_account::{CompressAs, CompressionInfo, CompressionState, Pack}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo, CompressionState, Pack}, - instruction::PackedAccounts, -}; +use light_sdk::instruction::PackedAccounts; use solana_pubkey::Pubkey; use super::shared::CompressibleTestFactory; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs index 5f21955680..ca7a10b574 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs @@ -10,8 +10,8 @@ //! Pack/Unpack traits are NOT generated because there are no Pubkey fields. use csdk_anchor_full_derived_test::LargeRecord; +use light_account::{CompressAs, CompressionInfo}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::interface::{CompressAs, CompressionInfo}; use super::shared::CompressibleTestFactory; use crate::generate_trait_tests; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs index 3aea42f537..f37f9c4e95 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs @@ -8,8 +8,8 @@ //! MinimalRecord has NO Pubkey fields, so Pack/Unpack traits are NOT generated. use csdk_anchor_full_derived_test::MinimalRecord; +use light_account::{CompressAs, CompressionInfo}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::interface::{CompressAs, CompressionInfo}; use super::shared::CompressibleTestFactory; use crate::generate_trait_tests; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs index 7249c896dc..d0c2c59be1 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs @@ -5,12 +5,9 @@ use std::borrow::Cow; +use light_account::{CompressAs, CompressedInitSpace, CompressionState, HasCompressionInfo, Size}; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - account::Size, - compressible::{CompressAs, CompressedInitSpace, CompressionState, HasCompressionInfo}, - LightDiscriminator, -}; +use light_sdk::LightDiscriminator; // ============================================================================= // Test Factory Trait @@ -125,10 +122,7 @@ pub fn assert_set_compression_info_none_works, + >(&program_id.to_bytes(), 0); + let light_config_pda = Pubkey::from(config_bytes); let program_metas = vec![ AccountMeta::new(payer.pubkey(), true), AccountMeta::new_readonly(light_config_pda, false), diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/d11_zero_copy_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/d11_zero_copy_test.rs index 80497c3ff1..2faa6594d8 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/d11_zero_copy_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/d11_zero_copy_test.rs @@ -41,6 +41,7 @@ use csdk_anchor_full_derived_test::d11_zero_copy::{ D11_ZC_VAULT_AUTH_SEED, D11_ZC_VAULT_SEED, }; +use light_account::IntoVariant; use light_client::interface::{ create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, CreateAccountsProofInput, InitializeRentFreeConfig, PdaSpec, @@ -51,7 +52,6 @@ use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, ProgramTestConfig, Rpc, }; -use light_sdk::interface::IntoVariant; use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; use solana_instruction::Instruction; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/failing_tests.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/failing_tests.rs index 05e8ab04b7..6a4742b67e 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/failing_tests.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/failing_tests.rs @@ -1,13 +1,14 @@ //! Security validation tests for decompression. //! //! Tests verify that invalid decompression attempts are correctly rejected. -//! Error codes reference: -//! - 2: InvalidInstructionData -//! - 3: InvalidAccountData -//! - 8: MissingRequiredSignature -//! - 11: NotEnoughAccountKeys -//! - 14: InvalidSeeds -//! - 16001: LightSdkError::ConstraintViolation +//! Error codes reference (LightSdkTypesError custom program error codes): +//! - 14017: FewerAccountsThanSystemAccounts +//! - 14035: ConstraintViolation +//! - 14038: InvalidRentSponsor +//! - 14043: InvalidInstructionData +//! - 14044: InvalidSeeds +//! - 14046: NotEnoughAccountKeys +//! - 14047: MissingRequiredSignature mod shared; @@ -19,6 +20,7 @@ use csdk_anchor_full_derived_test::{ D11_ZC_VAULT_SEED, }, }; +use light_account::IntoVariant; use light_client::interface::{ create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, CreateAccountsProofInput, InitializeRentFreeConfig, PdaSpec, @@ -29,7 +31,6 @@ use light_program_test::{ utils::assert::assert_rpc_error, ProgramTestConfig, Rpc, }; -use light_sdk::interface::IntoVariant; use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; use solana_instruction::{AccountMeta, Instruction}; @@ -166,7 +167,7 @@ impl FailingTestContext { // PDA DECOMPRESSION TESTS // ============================================================================= -/// Test: Wrong rent sponsor should fail with InvalidAccountData (3). +/// Test: Wrong rent sponsor should fail with InvalidRentSponsor (14038). /// Validates rent sponsor PDA derivation check in decompress.rs:160-169. #[tokio::test] async fn test_pda_wrong_rent_sponsor() { @@ -208,8 +209,8 @@ async fn test_pda_wrong_rent_sponsor() { .create_and_send_transaction(&decompress_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) .await; - // Should fail with InvalidRentSponsor (16050) - assert_rpc_error(result, 0, 16050).unwrap(); + // Should fail with InvalidRentSponsor (14038) + assert_rpc_error(result, 0, 14038).unwrap(); } /// Test: Double decompression should be a noop (idempotent). @@ -295,7 +296,7 @@ async fn test_pda_double_decompress_is_noop() { ); } -/// Test: Wrong config PDA should fail with ConstraintViolation (16001). +/// Test: Wrong config PDA should fail with ConstraintViolation (14035). /// Validates config check in config.rs:144-153. #[tokio::test] async fn test_pda_wrong_config() { @@ -336,15 +337,15 @@ async fn test_pda_wrong_config() { .await; // Should fail - the config validation will reject the wrong address - // This could be InvalidAccountData (3) since it fails to deserialize - assert_rpc_error(result, 0, 3).unwrap(); + // ConstraintViolation (14035) since deserialization/validation of the config account fails + assert_rpc_error(result, 0, 14035).unwrap(); } // ============================================================================= // COMMON PARAMETER TESTS // ============================================================================= -/// Test: system_accounts_offset out of bounds should fail with InvalidInstructionData (2). +/// Test: system_accounts_offset out of bounds should fail with InvalidInstructionData (14043). /// Validates bounds check in decompress.rs:175-177. #[tokio::test] async fn test_system_accounts_offset_out_of_bounds() { @@ -370,12 +371,12 @@ async fn test_system_accounts_offset_out_of_bounds() { .await .expect("create_load_instructions should succeed"); - // The instruction data format is: [discriminator(8)] [length(4)] [system_accounts_offset(1)] ... + // The instruction data format is: [discriminator(8)] [system_accounts_offset(1)] ... // Modify system_accounts_offset to be out of bounds if let Some(ix) = decompress_instructions.first_mut() { - // Byte 12 is system_accounts_offset (after 8-byte discriminator + 4-byte length) - if ix.data.len() > 12 { - ix.data[12] = 255; // Set to max u8, guaranteed out of bounds + // Byte 8 is system_accounts_offset (directly after 8-byte discriminator) + if ix.data.len() > 8 { + ix.data[8] = 255; // Set to max u8, guaranteed out of bounds } } @@ -384,11 +385,11 @@ async fn test_system_accounts_offset_out_of_bounds() { .create_and_send_transaction(&decompress_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) .await; - // Should fail with InvalidInstructionData (2) - assert_rpc_error(result, 0, 2).unwrap(); + // Should fail with InvalidInstructionData (14043) + assert_rpc_error(result, 0, 14043).unwrap(); } -/// Test: token_accounts_offset invalid should fail with NotEnoughAccountKeys (11). +/// Test: token_accounts_offset invalid should fail with InvalidInstructionData (14043). /// Validates bounds check in decompress.rs:178-181. #[tokio::test] async fn test_token_accounts_offset_invalid() { @@ -415,12 +416,12 @@ async fn test_token_accounts_offset_invalid() { .expect("create_load_instructions should succeed"); // The instruction data format is: - // [discriminator(8)] [length(4)] [system_accounts_offset(1)] [token_accounts_offset(1)] ... + // [discriminator(8)] [system_accounts_offset(1)] [token_accounts_offset(1)] ... // Modify token_accounts_offset to be larger than accounts.len() if let Some(ix) = decompress_instructions.first_mut() { - // Byte 13 is token_accounts_offset - if ix.data.len() > 13 { - ix.data[13] = 200; // Set to value larger than accounts + // Byte 9 is token_accounts_offset (after 8-byte discriminator + 1-byte system_accounts_offset) + if ix.data.len() > 9 { + ix.data[9] = 200; // Set to value larger than accounts } } @@ -429,8 +430,8 @@ async fn test_token_accounts_offset_invalid() { .create_and_send_transaction(&decompress_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) .await; - // Should fail with NotEnoughAccountKeys (11) - assert_rpc_error(result, 0, 11).unwrap(); + // Should fail with InvalidInstructionData (14043) - token_accounts_offset exceeds accounts count + assert_rpc_error(result, 0, 14043).unwrap(); } // ============================================================================= @@ -438,7 +439,7 @@ async fn test_token_accounts_offset_invalid() { // ============================================================================= /// Test: Removing required accounts should fail. -/// Error code 16031 is LightSdkError::CpiAccountsMissing. +/// Error code 14017 is FewerAccountsThanSystemAccounts. #[tokio::test] async fn test_missing_system_accounts() { let mut ctx = FailingTestContext::new().await; @@ -476,12 +477,12 @@ async fn test_missing_system_accounts() { .create_and_send_transaction(&decompress_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) .await; - // Should fail with NotEnoughAccountKeys (11) - gracefully handled missing accounts - assert_rpc_error(result, 0, 11).unwrap(); + // Should fail with FewerAccountsThanSystemAccounts (14017) - not enough remaining accounts + assert_rpc_error(result, 0, 14017).unwrap(); } /// Test: Wrong PDA account (mismatch between seeds and account) should fail. -/// When we try to create a PDA at a non-PDA address, we get PrivilegeEscalation (19). +/// When seeds don't match the account, we get InvalidSeeds (14044). #[tokio::test] async fn test_pda_account_mismatch() { let mut ctx = FailingTestContext::new().await; @@ -519,9 +520,9 @@ async fn test_pda_account_mismatch() { .create_and_send_transaction(&decompress_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) .await; - // Should fail with InvalidSeeds (14) - PDA derivation validation catches + // Should fail with InvalidSeeds (14044) - PDA derivation validation catches // the mismatch before attempting CPI - assert_rpc_error(result, 0, 14).unwrap(); + assert_rpc_error(result, 0, 14044).unwrap(); } /// Test: Fee payer not a signer should fail with MissingRequiredSignature (8). diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/instruction_decoder_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/instruction_decoder_test.rs index 1c2b8c7744..68c1a2dc9b 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/instruction_decoder_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/instruction_decoder_test.rs @@ -186,8 +186,7 @@ fn test_enhanced_decoder_params_decoding() { instruction_accounts::CreateTwoMintsParams, CsdkTestInstructionDecoder, }; use light_compressed_account::instruction_data::compressed_proof::ValidityProof; - use light_compressible::CreateAccountsProof; - use light_sdk_types::instruction::PackedAddressTreeInfo; + use light_sdk_types::{instruction::PackedAddressTreeInfo, interface::CreateAccountsProof}; let decoder = CsdkTestInstructionDecoder; @@ -394,9 +393,8 @@ fn test_attribute_macro_decoder_with_instruction_data() { instruction_accounts::CreateTwoMintsParams, CsdkAnchorFullDerivedTestInstructionDecoder, }; use light_compressed_account::instruction_data::compressed_proof::ValidityProof; - use light_compressible::CreateAccountsProof; use light_program_test::logging::InstructionDecoder; - use light_sdk_types::instruction::PackedAddressTreeInfo; + use light_sdk_types::{instruction::PackedAddressTreeInfo, interface::CreateAccountsProof}; let decoder = CsdkAnchorFullDerivedTestInstructionDecoder; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs index 0363047387..23afe9a1f5 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs @@ -9,6 +9,7 @@ mod shared; use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::LightAccountVariant; +use light_account::IntoVariant; use light_client::interface::{ create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, CreateAccountsProofInput, InitializeRentFreeConfig, PdaSpec, @@ -18,7 +19,6 @@ use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, ProgramTestConfig, Rpc, }; -use light_sdk::interface::IntoVariant; /// Light Token's rent sponsor - used for Light Token operations use light_token::instruction::LIGHT_TOKEN_RENT_SPONSOR; use solana_instruction::Instruction; @@ -42,7 +42,7 @@ struct TestContext { impl TestContext { async fn new() -> Self { - use light_sdk::utils::derive_rent_sponsor_pda; + use light_account::derive_rent_sponsor_pda; let program_id = csdk_anchor_full_derived_test::ID; let mut config = ProgramTestConfig::new_v2( @@ -1693,7 +1693,7 @@ async fn test_d5_light_token() { // Full lifecycle: compression + decompression use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D5TokenVaultSeeds; - use light_sdk::interface::token::TokenDataWithSeeds; + use light_account::TokenDataWithSeeds; // Warp time to trigger compression ctx.rpc @@ -1797,7 +1797,7 @@ async fn test_d5_all_markers() { use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ D5AllRecordSeeds, D5AllVaultSeeds, }; - use light_sdk::interface::token::TokenDataWithSeeds; + use light_account::TokenDataWithSeeds; // Warp time to trigger compression of BOTH accounts ctx.rpc @@ -1894,7 +1894,7 @@ async fn test_d7_light_token_config() { // Full lifecycle: compression + decompression use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D7LightTokenVaultSeeds; - use light_sdk::interface::token::TokenDataWithSeeds; + use light_account::TokenDataWithSeeds; // Warp time to trigger compression ctx.rpc @@ -1999,7 +1999,7 @@ async fn test_d7_all_names() { use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ D7AllRecordSeeds, D7AllVaultSeeds, }; - use light_sdk::interface::token::TokenDataWithSeeds; + use light_account::TokenDataWithSeeds; // Warp time to trigger compression of BOTH accounts ctx.rpc diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs index 69b6c73f1a..83a2fc7356 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs @@ -1,12 +1,12 @@ #![allow(dead_code)] // Shared test utilities for csdk-anchor-full-derived-test +use light_account::derive_rent_sponsor_pda; use light_client::{indexer::Indexer, interface::InitializeRentFreeConfig, rpc::Rpc}; use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest}, ProgramTestConfig, }; -use light_sdk::utils::derive_rent_sponsor_pda; use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; /// Shared test context for csdk-anchor-full-derived-test @@ -358,9 +358,9 @@ pub async fn setup_create_mint( /// Build expected CompressionInfo, extracting only runtime fields from actual. /// Validates all config-derived fields against expected defaults. pub fn expected_compression_info( - actual: &light_sdk::compressible::CompressionInfo, -) -> light_sdk::compressible::CompressionInfo { - light_sdk::compressible::CompressionInfo { + actual: &light_account::CompressionInfo, +) -> light_account::CompressionInfo { + light_account::CompressionInfo { last_claimed_slot: actual.last_claimed_slot, lamports_per_write: 5000, config_version: 1, diff --git a/sdk-tests/manual-test/src/all/derived.rs b/sdk-tests/manual-test/src/all/derived.rs deleted file mode 100644 index 2a764b9f2a..0000000000 --- a/sdk-tests/manual-test/src/all/derived.rs +++ /dev/null @@ -1,307 +0,0 @@ -//! Derived code for create_all instruction. -//! -//! This implements LightPreInit/LightFinalize for creating all account types: -//! - 2 PDAs (Borsh + ZeroCopy) via `invoke_write_to_cpi_context_first()` -//! - 1 Mint via `invoke_create_mints()` with cpi_context_offset -//! - 1 Token Vault via `CreateTokenAccountCpi` -//! - 1 ATA via `CreateTokenAtaCpi` - -use anchor_lang::prelude::*; -use light_compressed_account::instruction_data::{ - cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, -}; -use light_sdk::{ - cpi::{v2::CpiAccounts, CpiAccountsConfig, InvokeLightSystemProgram}, - error::LightSdkError, - instruction::PackedAddressTreeInfoExt, - interface::{prepare_compressed_account_on_init, LightAccount, LightFinalize, LightPreInit}, - sdk_types::CpiContextWriteAccounts, -}; -use light_token::{ - compressible::{invoke_create_mints, CreateMintsInfraAccounts}, - instruction::{ - derive_mint_compressed_address, find_mint_address, - CreateMintsParams as SdkCreateMintsParams, CreateTokenAccountCpi, CreateTokenAtaCpi, - SingleMintParams, - }, -}; -use solana_account_info::AccountInfo; -use solana_program_error::ProgramError; - -use super::accounts::{ - CreateAllAccounts, CreateAllParams, ALL_MINT_SIGNER_SEED, ALL_TOKEN_VAULT_SEED, -}; - -// ============================================================================ -// LightPreInit Implementation - Creates all accounts at START of instruction -// ============================================================================ - -impl<'info> LightPreInit<'info, CreateAllParams> for CreateAllAccounts<'info> { - fn light_pre_init( - &mut self, - remaining_accounts: &[AccountInfo<'info>], - params: &CreateAllParams, - ) -> std::result::Result { - use light_sdk::interface::config::LightConfig; - use solana_program::{clock::Clock, sysvar::Sysvar}; - - // Constants for this instruction - const NUM_LIGHT_PDAS: usize = 2; - const NUM_LIGHT_MINTS: usize = 1; - const WITH_CPI_CONTEXT: bool = NUM_LIGHT_PDAS > 0 && NUM_LIGHT_MINTS > 0; // true - - // ==================================================================== - // 1. Build CPI accounts with cpi_context config - // ==================================================================== - let system_accounts_offset = params.create_accounts_proof.system_accounts_offset as usize; - if remaining_accounts.len() < system_accounts_offset { - return Err(LightSdkError::FewerAccountsThanSystemAccounts); - } - let config = CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER); - let cpi_accounts = CpiAccounts::new_with_config( - &self.payer, - &remaining_accounts[system_accounts_offset..], - config, - ); - - // ==================================================================== - // 2. Get address tree info - // ==================================================================== - let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; - let address_tree_pubkey = address_tree_info - .get_tree_pubkey(&cpi_accounts) - .map_err(|_| LightSdkError::from(ProgramError::InvalidAccountData))?; - let output_tree_index = params.create_accounts_proof.output_state_tree_index; - - // ==================================================================== - // 3. Load config, get current slot - // ==================================================================== - let light_config = LightConfig::load_checked(&self.compression_config, &crate::ID) - .map_err(|_| LightSdkError::from(ProgramError::InvalidAccountData))?; - let current_slot = Clock::get() - .map_err(|_| LightSdkError::from(ProgramError::InvalidAccountData))? - .slot; - - // ==================================================================== - // 4. Create PDAs via invoke_write_to_cpi_context_first() - // ==================================================================== - { - // CPI context for PDAs - set to first() since we have mints coming after - let cpi_context = CompressedCpiContext::first(); - let mut new_address_params = Vec::with_capacity(NUM_LIGHT_PDAS); - let mut account_infos = Vec::with_capacity(NUM_LIGHT_PDAS); - - // 4a. Prepare Borsh PDA (index 0) - let borsh_record_key = self.borsh_record.key(); - prepare_compressed_account_on_init( - &borsh_record_key, - &address_tree_pubkey, - address_tree_info, - output_tree_index, - 0, // assigned_account_index = 0 - &crate::ID, - &mut new_address_params, - &mut account_infos, - )?; - self.borsh_record - .set_decompressed(&light_config, current_slot); - - // 4b. Prepare ZeroCopy PDA (index 1) - let zero_copy_record_key = self.zero_copy_record.key(); - prepare_compressed_account_on_init( - &zero_copy_record_key, - &address_tree_pubkey, - address_tree_info, - output_tree_index, - 1, // assigned_account_index = 1 - &crate::ID, - &mut new_address_params, - &mut account_infos, - )?; - { - let mut record = self - .zero_copy_record - .load_init() - .map_err(|_| LightSdkError::from(ProgramError::AccountBorrowFailed))?; - record.set_decompressed(&light_config, current_slot); - } - - // 4c. Build instruction data and write to CPI context (doesn't execute yet) - let instruction_data = InstructionDataInvokeCpiWithAccountInfo { - mode: 1, // V2 mode - bump: crate::LIGHT_CPI_SIGNER.bump, - invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), - compress_or_decompress_lamports: 0, - is_compress: false, - with_cpi_context: WITH_CPI_CONTEXT, - with_transaction_hash: false, - cpi_context, - proof: params.create_accounts_proof.proof.0, - new_address_params, - account_infos, - read_only_addresses: vec![], - read_only_accounts: vec![], - }; - - // Write to CPI context first (combined execution happens with mints) - let cpi_context_accounts = CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts.authority().map_err(LightSdkError::from)?, - cpi_context: cpi_accounts.cpi_context().map_err(LightSdkError::from)?, - cpi_signer: crate::LIGHT_CPI_SIGNER, - }; - instruction_data - .invoke_write_to_cpi_context_first(cpi_context_accounts) - .map_err(LightSdkError::from)?; - } - - // ==================================================================== - // 5. Create Mint via invoke_create_mints() with offset - // ==================================================================== - { - let authority = self.authority.key(); - let mint_signer_key = self.mint_signer.key(); - - // Derive mint PDA - let (mint_pda, mint_bump) = find_mint_address(&solana_pubkey::Pubkey::new_from_array( - mint_signer_key.to_bytes(), - )); - - // Derive compression address - let compression_address = derive_mint_compressed_address( - &solana_pubkey::Pubkey::new_from_array(mint_signer_key.to_bytes()), - &solana_pubkey::Pubkey::new_from_array(address_tree_pubkey.to_bytes()), - ); - - // Build mint signer seeds - let mint_signer_seeds: &[&[u8]] = &[ - ALL_MINT_SIGNER_SEED, - authority.as_ref(), - &[params.mint_signer_bump], - ]; - - // Build SingleMintParams - let sdk_mints: [SingleMintParams<'_>; NUM_LIGHT_MINTS] = [SingleMintParams { - decimals: 6, // mint::decimals = 6 - address_merkle_tree_root_index: address_tree_info.root_index, - mint_authority: solana_pubkey::Pubkey::new_from_array(authority.to_bytes()), - compression_address, - mint: mint_pda, - bump: mint_bump, - freeze_authority: None, - mint_seed_pubkey: solana_pubkey::Pubkey::new_from_array(mint_signer_key.to_bytes()), - authority_seeds: None, - mint_signer_seeds: Some(mint_signer_seeds), - token_metadata: None, - }]; - - // Get state_tree_index - let state_tree_index = params - .create_accounts_proof - .state_tree_index - .ok_or(LightSdkError::from(ProgramError::InvalidArgument))?; - - let proof = params - .create_accounts_proof - .proof - .0 - .ok_or(LightSdkError::from(ProgramError::InvalidArgument))?; - - // Build SDK params with cpi_context_offset - let sdk_params = SdkCreateMintsParams::new(&sdk_mints, proof) - .with_output_queue_index(params.create_accounts_proof.output_state_tree_index) - .with_address_tree_index(address_tree_info.address_merkle_tree_pubkey_index) - .with_state_tree_index(state_tree_index) - .with_cpi_context_offset(NUM_LIGHT_PDAS as u8); // Offset by PDA count - - // Build infra accounts - let infra = CreateMintsInfraAccounts { - fee_payer: self.payer.to_account_info(), - compressible_config: self.compressible_config.clone(), - rent_sponsor: self.rent_sponsor.clone(), - cpi_authority: self.cpi_authority.clone(), - }; - - // Build mint account arrays - let mint_seed_accounts = [self.mint_signer.to_account_info()]; - let mint_accounts = [self.mint.to_account_info()]; - - // This executes the combined CPI (PDAs + Mint) - invoke_create_mints( - &mint_seed_accounts, - &mint_accounts, - sdk_params, - infra, - &cpi_accounts, - )?; - } - - // ==================================================================== - // 6. Create Token Vault via CreateTokenAccountCpi - // ==================================================================== - { - let mint_key = self.mint.key(); - let vault_seeds: &[&[u8]] = &[ - ALL_TOKEN_VAULT_SEED, - mint_key.as_ref(), - &[params.token_vault_bump], - ]; - - CreateTokenAccountCpi { - payer: self.payer.to_account_info(), - account: self.token_vault.to_account_info(), - mint: self.mint.to_account_info(), - owner: *self.vault_owner.key, - } - .rent_free( - self.compressible_config.clone(), - self.rent_sponsor.clone(), - self.system_program.to_account_info(), - &crate::ID, - ) - .invoke_signed(vault_seeds)?; - } - - // ==================================================================== - // 7. Create ATA via CreateTokenAtaCpi - // ==================================================================== - { - let (_, ata_bump) = light_token::instruction::derive_associated_token_account( - self.ata_owner.key, - self.mint.key, - ); - - CreateTokenAtaCpi { - payer: self.payer.to_account_info(), - owner: self.ata_owner.clone(), - mint: self.mint.to_account_info(), - ata: self.user_ata.to_account_info(), - bump: ata_bump, - } - .rent_free( - self.compressible_config.clone(), - self.rent_sponsor.clone(), - self.system_program.to_account_info(), - ) - .invoke()?; - } - - Ok(WITH_CPI_CONTEXT) - } -} - -// ============================================================================ -// LightFinalize Implementation - No-op for this flow -// ============================================================================ - -impl<'info> LightFinalize<'info, CreateAllParams> for CreateAllAccounts<'info> { - fn light_finalize( - &mut self, - _remaining_accounts: &[AccountInfo<'info>], - _params: &CreateAllParams, - _has_pre_init: bool, - ) -> std::result::Result<(), LightSdkError> { - // All accounts were created in light_pre_init - Ok(()) - } -} diff --git a/sdk-tests/manual-test/src/ata/derived.rs b/sdk-tests/manual-test/src/ata/derived.rs deleted file mode 100644 index e5ea0e0d8d..0000000000 --- a/sdk-tests/manual-test/src/ata/derived.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Derived code - what the macro would generate for associated token accounts. - -use anchor_lang::prelude::*; -use light_sdk::{ - error::LightSdkError, - interface::{LightFinalize, LightPreInit}, -}; -use light_token::instruction::CreateTokenAtaCpi; -use solana_account_info::AccountInfo; - -use super::accounts::{CreateAtaAccounts, CreateAtaParams}; - -// ============================================================================ -// LightPreInit Implementation - Creates ATA at START of instruction -// ============================================================================ - -impl<'info> LightPreInit<'info, CreateAtaParams> for CreateAtaAccounts<'info> { - fn light_pre_init( - &mut self, - _remaining_accounts: &[AccountInfo<'info>], - _params: &CreateAtaParams, - ) -> std::result::Result { - // Derive the ATA bump on-chain - let (_, bump) = light_token::instruction::derive_associated_token_account( - self.ata_owner.key, - self.mint.key, - ); - - // Create ATA via CPI with idempotent + rent-free mode - // NOTE: Unlike token vaults, ATAs use .invoke() not .invoke_signed() - // because ATAs are derived from [owner, token_program, mint], not program PDAs - CreateTokenAtaCpi { - payer: self.payer.to_account_info(), - owner: self.ata_owner.clone(), - mint: self.mint.clone(), - ata: self.user_ata.to_account_info(), - bump, - } - .idempotent() // Safe: won't fail if ATA already exists - .rent_free( - self.compressible_config.clone(), - self.rent_sponsor.clone(), - self.system_program.to_account_info(), - ) - .invoke()?; - - // ATAs don't use CPI context, return false - Ok(false) - } -} - -// ============================================================================ -// LightFinalize Implementation - No-op for ATA only flow -// ============================================================================ - -impl<'info> LightFinalize<'info, CreateAtaParams> for CreateAtaAccounts<'info> { - fn light_finalize( - &mut self, - _remaining_accounts: &[AccountInfo<'info>], - _params: &CreateAtaParams, - _has_pre_init: bool, - ) -> std::result::Result<(), LightSdkError> { - Ok(()) - } -} diff --git a/sdk-tests/manual-test/src/pda/derived_accounts.rs b/sdk-tests/manual-test/src/pda/derived_accounts.rs deleted file mode 100644 index 81d7e5a74d..0000000000 --- a/sdk-tests/manual-test/src/pda/derived_accounts.rs +++ /dev/null @@ -1,367 +0,0 @@ -use anchor_lang::prelude::*; -use light_compressed_account::instruction_data::{ - cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, -}; -use light_sdk::{ - cpi::{v2::CpiAccounts, CpiAccountsConfig, InvokeLightSystemProgram}, - error::LightSdkError, - instruction::{PackedAccounts, PackedAddressTreeInfoExt}, - interface::{ - prepare_compressed_account_on_init, LightAccount, LightAccountVariantTrait, LightFinalize, - LightPreInit, PackedLightAccountVariantTrait, - }, - light_account_checks::packed_accounts::ProgramPackedAccounts, - sdk_types::CpiContextWriteAccounts, -}; -use solana_program_error::ProgramError; - -use super::{ - accounts::{CreatePda, CreatePdaParams}, - derived_state::PackedMinimalRecord, - state::MinimalRecord, -}; - -// ============================================================================ -// Compile-time Size Validation (800-byte limit for compressed accounts) -// ============================================================================ - -const _: () = { - // Use Anchor's Space trait (from #[derive(InitSpace)]) - const COMPRESSED_SIZE: usize = 8 + ::INIT_SPACE; - assert!( - COMPRESSED_SIZE <= 800, - "Compressed account 'MinimalRecord' exceeds 800-byte compressible account size limit" - ); -}; - -// ============================================================================ -// Manual LightPreInit Implementation -// ============================================================================ - -impl<'info> LightPreInit<'info, CreatePdaParams> for CreatePda<'info> { - fn light_pre_init( - &mut self, - remaining_accounts: &[AccountInfo<'info>], - params: &CreatePdaParams, - ) -> std::result::Result { - use light_sdk::interface::{config::LightConfig, LightAccount}; - use solana_program::{clock::Clock, sysvar::Sysvar}; - use solana_program_error::ProgramError; - - // 1. Build CPI accounts (slice remaining_accounts at system_accounts_offset) - let system_accounts_offset = params.create_accounts_proof.system_accounts_offset as usize; - if remaining_accounts.len() < system_accounts_offset { - return Err(LightSdkError::FewerAccountsThanSystemAccounts); - } - let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); - let cpi_accounts = CpiAccounts::new_with_config( - &self.fee_payer, - &remaining_accounts[system_accounts_offset..], - config, - ); - - // 2. Get address tree pubkey from packed tree info - let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; - let address_tree_pubkey = address_tree_info - .get_tree_pubkey(&cpi_accounts) - .map_err(|_| LightSdkError::from(ProgramError::InvalidAccountData))?; - let output_tree_index = params.create_accounts_proof.output_state_tree_index; - let current_account_index: u8 = 0; - // Is true if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. - const WITH_CPI_CONTEXT: bool = false; - - const NUM_LIGHT_PDAS: usize = 1; - - // 6. Set compression_info from config - let light_config = LightConfig::load_checked(&self.compression_config, &crate::ID) - .map_err(|_| LightSdkError::from(ProgramError::InvalidAccountData))?; - let current_slot = Clock::get() - .map_err(|_| LightSdkError::from(ProgramError::InvalidAccountData))? - .slot; - // Dynamic derived light pda specific. Only exists if NUM_LIGHT_PDAS > 0 - // ===================================================================== - { - // Is first if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. - let cpi_context = if WITH_CPI_CONTEXT { - CompressedCpiContext::first() - } else { - CompressedCpiContext::default() - }; - let mut new_address_params = Vec::with_capacity(NUM_LIGHT_PDAS); - let mut account_infos = Vec::with_capacity(NUM_LIGHT_PDAS); - // 3. Prepare compressed account using helper function - // Dynamic code 0-N variants depending on the accounts struct - // ===================================================================== - prepare_compressed_account_on_init( - &self.record.key(), - &address_tree_pubkey, - address_tree_info, - output_tree_index, - current_account_index, - &crate::ID, - &mut new_address_params, - &mut account_infos, - )?; - self.record.set_decompressed(&light_config, current_slot); - // ===================================================================== - - // current_account_index += 1; - // For multiple accounts, repeat the pattern: - // let prepared2 = prepare_compressed_account_on_init(..., current_account_index, ...)?; - // current_account_index += 1; - - // 4. Build instruction data manually (no builder pattern) - let instruction_data = InstructionDataInvokeCpiWithAccountInfo { - mode: 1, // V2 mode - bump: crate::LIGHT_CPI_SIGNER.bump, - invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), - compress_or_decompress_lamports: 0, - is_compress: false, - with_cpi_context: WITH_CPI_CONTEXT, - with_transaction_hash: false, - cpi_context, - proof: params.create_accounts_proof.proof.0, - new_address_params, - account_infos, - read_only_addresses: vec![], - read_only_accounts: vec![], - }; - if !WITH_CPI_CONTEXT { - // 5. Invoke Light System Program CPI - instruction_data - .invoke(cpi_accounts) - .map_err(LightSdkError::from)?; - } else { - // For flows that combine light mints with light PDAs, write to CPI context first. - // The authority and cpi_context accounts must be provided in remaining_accounts. - let cpi_context_accounts = CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts.authority().map_err(LightSdkError::from)?, - cpi_context: cpi_accounts.cpi_context().map_err(LightSdkError::from)?, - cpi_signer: crate::LIGHT_CPI_SIGNER, - }; - instruction_data - .invoke_write_to_cpi_context_first(cpi_context_accounts) - .map_err(LightSdkError::from)?; - } - } - // ===================================================================== - Ok(false) // No mints, so no CPI context write - } -} - -// ============================================================================ -// Manual LightFinalize Implementation (no-op for PDA-only flow) -// ============================================================================ - -impl<'info> LightFinalize<'info, CreatePdaParams> for CreatePda<'info> { - fn light_finalize( - &mut self, - _remaining_accounts: &[AccountInfo<'info>], - _params: &CreatePdaParams, - _has_pre_init: bool, - ) -> std::result::Result<(), LightSdkError> { - // No-op for PDA-only flow - compression CPI already executed in light_pre_init - Ok(()) - } -} - -// ============================================================================ -// Seeds Structs -// Extracted from: seeds = [b"minimal_record", params.owner.as_ref()] -// ============================================================================ - -/// Seeds for MinimalRecord PDA. -/// Contains the dynamic seed values (static prefix "minimal_record" is in seed_refs). -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct MinimalRecordSeeds { - pub owner: Pubkey, - pub nonce: u64, -} - -/// Packed seeds with u8 indices instead of Pubkeys. -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct PackedMinimalRecordSeeds { - pub owner_idx: u8, - pub nonce_bytes: [u8; 8], - pub bump: u8, -} - -// ============================================================================ -// Variant Structs -// ============================================================================ - -/// Full variant combining seeds + data. -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct MinimalRecordVariant { - pub seeds: MinimalRecordSeeds, - pub data: MinimalRecord, -} - -/// Packed variant for efficient serialization. -/// Contains packed seeds and data with u8 indices for Pubkey deduplication. -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct PackedMinimalRecordVariant { - pub seeds: PackedMinimalRecordSeeds, - pub data: PackedMinimalRecord, -} - -// ============================================================================ -// LightAccountVariant Implementation -// ============================================================================ - -impl LightAccountVariantTrait<4> for MinimalRecordVariant { - const PROGRAM_ID: Pubkey = crate::ID; - - type Seeds = MinimalRecordSeeds; - type Data = MinimalRecord; - type Packed = PackedMinimalRecordVariant; - - fn data(&self) -> &Self::Data { - &self.data - } - - /// Get seed values as owned byte vectors for PDA derivation. - /// Generated from: seeds = [b"minimal_record", params.owner.as_ref(), ¶ms.nonce.to_le_bytes()] - fn seed_vec(&self) -> Vec> { - vec![ - b"minimal_record".to_vec(), - self.seeds.owner.to_bytes().to_vec(), - self.seeds.nonce.to_le_bytes().to_vec(), - ] - } - - /// Get seed references with bump for CPI signing. - /// Note: For unpacked variants with computed bytes (like nonce.to_le_bytes()), - /// we cannot return references to temporaries. Use the packed variant instead. - fn seed_refs_with_bump<'a>(&'a self, _bump_storage: &'a [u8; 1]) -> [&'a [u8]; 4] { - // The packed variant stores nonce_bytes as [u8; 8], so it can return references. - // This unpacked variant computes nonce.to_le_bytes() which creates a temporary. - panic!("Use PackedMinimalRecordVariant::seed_refs_with_bump instead") - } -} - -// ============================================================================ -// PackedLightAccountVariant Implementation -// ============================================================================ - -impl PackedLightAccountVariantTrait<4> for PackedMinimalRecordVariant { - type Unpacked = MinimalRecordVariant; - - const ACCOUNT_TYPE: light_sdk::interface::AccountType = - ::ACCOUNT_TYPE; - - fn bump(&self) -> u8 { - self.seeds.bump - } - - fn unpack(&self, accounts: &[AccountInfo]) -> Result { - let owner = accounts - .get(self.seeds.owner_idx as usize) - .ok_or(anchor_lang::error::ErrorCode::AccountNotEnoughKeys)?; - - // Build ProgramPackedAccounts for LightAccount::unpack - let packed_accounts = ProgramPackedAccounts { accounts }; - let data = MinimalRecord::unpack(&self.data, &packed_accounts) - .map_err(|_| anchor_lang::error::ErrorCode::InvalidProgramId)?; // TODO: propagate error - - Ok(MinimalRecordVariant { - seeds: MinimalRecordSeeds { - owner: *owner.key, - nonce: u64::from_le_bytes(self.seeds.nonce_bytes), - }, - data, - }) - } - - fn seed_refs_with_bump<'a>( - &'a self, - accounts: &'a [AccountInfo], - bump_storage: &'a [u8; 1], - ) -> std::result::Result<[&'a [u8]; 4], ProgramError> { - let owner = accounts - .get(self.seeds.owner_idx as usize) - .ok_or(ProgramError::InvalidAccountData)?; - Ok([ - b"minimal_record", - owner.key.as_ref(), - &self.seeds.nonce_bytes, - bump_storage, - ]) - } - - fn into_in_token_data( - &self, - _tree_info: &light_sdk::instruction::PackedStateTreeInfo, - _output_queue_index: u8, - ) -> Result { - Err(ProgramError::InvalidAccountData.into()) - } - - fn into_in_tlv( - &self, - ) -> Result>> { - Ok(None) - } -} - -// ============================================================================ -// IntoVariant Implementation for Seeds (client-side API) -// ============================================================================ - -/// Implement IntoVariant to allow building variant from seeds + compressed data. -/// This enables the high-level `create_load_instructions` API. -#[cfg(not(target_os = "solana"))] -impl light_sdk::interface::IntoVariant for MinimalRecordSeeds { - fn into_variant( - self, - data: &[u8], - ) -> std::result::Result { - // Deserialize the compressed data (which includes compression_info) - let record: MinimalRecord = AnchorDeserialize::deserialize(&mut &data[..]) - .map_err(|_| anchor_lang::error::ErrorCode::AccountDidNotDeserialize)?; - - // Verify the owner in data matches the seed - if record.owner != self.owner { - return Err(anchor_lang::error::ErrorCode::ConstraintSeeds.into()); - } - - Ok(MinimalRecordVariant { - seeds: self, - data: record, - }) - } -} - -// ============================================================================ -// Pack Implementation for MinimalRecordVariant (client-side API) -// ============================================================================ - -/// Implement Pack trait to allow MinimalRecordVariant to be used with `create_load_instructions`. -/// Transforms the variant into PackedLightAccountVariant for efficient serialization. -#[cfg(not(target_os = "solana"))] -impl light_sdk::compressible::Pack for MinimalRecordVariant { - type Packed = crate::derived_variants::PackedLightAccountVariant; - - fn pack( - &self, - accounts: &mut PackedAccounts, - ) -> std::result::Result { - use light_sdk::interface::LightAccountVariantTrait; - let (_, bump) = self.derive_pda(); - let packed_data = self - .data - .pack(accounts) - .map_err(|_| ProgramError::InvalidAccountData)?; - Ok( - crate::derived_variants::PackedLightAccountVariant::MinimalRecord { - seeds: PackedMinimalRecordSeeds { - owner_idx: accounts.insert_or_get(self.seeds.owner), - nonce_bytes: self.seeds.nonce.to_le_bytes(), - bump, - }, - data: packed_data, - }, - ) - } -} diff --git a/sdk-tests/manual-test/src/token_account/derived.rs b/sdk-tests/manual-test/src/token_account/derived.rs deleted file mode 100644 index b245287943..0000000000 --- a/sdk-tests/manual-test/src/token_account/derived.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Derived code - what the macro would generate for token accounts. - -use anchor_lang::prelude::*; -use light_sdk::{ - error::LightSdkError, - interface::{LightFinalize, LightPreInit}, - Pack, Unpack, -}; -use light_token::instruction::CreateTokenAccountCpi; -use solana_account_info::AccountInfo; - -use super::accounts::{CreateTokenVaultAccounts, CreateTokenVaultParams, TOKEN_VAULT_SEED}; - -// ============================================================================ -// LightPreInit Implementation - Creates token account at START of instruction -// ============================================================================ - -impl<'info> LightPreInit<'info, CreateTokenVaultParams> for CreateTokenVaultAccounts<'info> { - fn light_pre_init( - &mut self, - _remaining_accounts: &[AccountInfo<'info>], - params: &CreateTokenVaultParams, - ) -> std::result::Result { - // Build PDA seeds: [TOKEN_VAULT_SEED, mint.key(), &[bump]] - let mint_key = self.mint.key(); - let vault_seeds: &[&[u8]] = &[TOKEN_VAULT_SEED, mint_key.as_ref(), &[params.vault_bump]]; - - // Create token account via CPI with rent-free mode - CreateTokenAccountCpi { - payer: self.payer.to_account_info(), - account: self.token_vault.to_account_info(), - mint: self.mint.clone(), - owner: *self.vault_owner.key, - } - .rent_free( - self.compressible_config.clone(), - self.rent_sponsor.clone(), - self.system_program.to_account_info(), - &crate::ID, - ) - .invoke_signed(vault_seeds)?; - - // Token accounts don't use CPI context, return false - Ok(false) - } -} - -// ============================================================================ -// LightFinalize Implementation - No-op for token account only flow -// ============================================================================ - -impl<'info> LightFinalize<'info, CreateTokenVaultParams> for CreateTokenVaultAccounts<'info> { - fn light_finalize( - &mut self, - _remaining_accounts: &[AccountInfo<'info>], - _params: &CreateTokenVaultParams, - _has_pre_init: bool, - ) -> std::result::Result<(), LightSdkError> { - Ok(()) - } -} -/* inside of in_tlv for (i, token) in params.token_accounts.iter().enumerate() { - if let Some(extension) = token.extension.clone() { - vec[i] = Some(vec![ExtensionInstructionData::CompressedOnly(extension)]); - } -}*/ -#[allow(dead_code)] -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct TokenVaultSeeds { - pub mint: Pubkey, -} - -impl Pack for TokenVaultSeeds { - type Packed = PackedTokenVaultSeeds; - fn pack( - &self, - remaining_accounts: &mut light_sdk::instruction::PackedAccounts, - ) -> std::result::Result { - Ok(PackedTokenVaultSeeds { - mint_idx: remaining_accounts.insert_or_get(self.mint), - bump: 0, - }) - } -} - -#[allow(dead_code)] -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct PackedTokenVaultSeeds { - pub mint_idx: u8, - pub bump: u8, -} - -impl Unpack for PackedTokenVaultSeeds { - type Unpacked = TokenVaultSeeds; - - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - let mint = *remaining_accounts - .get(self.mint_idx as usize) - .ok_or(ProgramError::InvalidAccountData)? - .key; - Ok(TokenVaultSeeds { mint }) - } -} diff --git a/sdk-tests/manual-test/src/two_mints/derived.rs b/sdk-tests/manual-test/src/two_mints/derived.rs deleted file mode 100644 index 668fcb498d..0000000000 --- a/sdk-tests/manual-test/src/two_mints/derived.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Derived code - what the macro would generate. -//! Contains LightPreInit/LightFinalize trait implementations. - -use anchor_lang::prelude::*; -use light_sdk::{ - cpi::{v2::CpiAccounts, CpiAccountsConfig}, - error::LightSdkError, - instruction::PackedAddressTreeInfoExt, - interface::{LightFinalize, LightPreInit}, -}; -use light_token::{ - compressible::{invoke_create_mints, CreateMintsInfraAccounts}, - instruction::{ - derive_mint_compressed_address, find_mint_address, - CreateMintsParams as SdkCreateMintsParams, SingleMintParams, - }, -}; -use solana_account_info::AccountInfo; - -use super::accounts::{ - CreateDerivedMintsAccounts, CreateDerivedMintsParams, MINT_SIGNER_0_SEED, MINT_SIGNER_1_SEED, -}; - -// ============================================================================ -// LightPreInit Implementation - Creates mints at START of instruction -// ============================================================================ - -impl<'info> LightPreInit<'info, CreateDerivedMintsParams> for CreateDerivedMintsAccounts<'info> { - fn light_pre_init( - &mut self, - remaining_accounts: &[AccountInfo<'info>], - params: &CreateDerivedMintsParams, - ) -> std::result::Result { - use solana_program_error::ProgramError; - - // ==================================================================== - // STATIC BOILERPLATE (same across all LightPreInit implementations) - // ==================================================================== - - // 1. Build CPI accounts (slice remaining_accounts at system_accounts_offset) - let system_accounts_offset = params.create_accounts_proof.system_accounts_offset as usize; - if remaining_accounts.len() < system_accounts_offset { - return Err(LightSdkError::FewerAccountsThanSystemAccounts); - } - let config = CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER); - let cpi_accounts = CpiAccounts::new_with_config( - &self.payer, - &remaining_accounts[system_accounts_offset..], - config, - ); - - // 2. Get address tree pubkey from packed tree info - let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; - let address_tree_pubkey = address_tree_info - .get_tree_pubkey(&cpi_accounts) - .map_err(|_| LightSdkError::from(ProgramError::InvalidAccountData))?; - - // Constants for this instruction (mirrors macro-generated code) - const NUM_LIGHT_MINTS: usize = 2; - const NUM_LIGHT_PDAS: usize = 0; // Set to actual PDA count when combining PDAs + mints - #[allow(clippy::absurd_extreme_comparisons)] - const WITH_CPI_CONTEXT: bool = NUM_LIGHT_PDAS > 0 && NUM_LIGHT_MINTS > 0; // true if combining mints + PDAs - - // ==================================================================== - // DYNAMIC CODE (specific to this accounts struct) - // ==================================================================== - { - let authority = self.authority.key(); - - // Get mint signer pubkeys from accounts - let mint_signer_0 = self.mint_signer_0.key(); - let mint_signer_1 = self.mint_signer_1.key(); - - // Derive mint PDAs (light-token derives mint PDA from mint_signer) - let (mint_0_pda, mint_0_bump) = find_mint_address( - &solana_pubkey::Pubkey::new_from_array(mint_signer_0.to_bytes()), - ); - let (mint_1_pda, mint_1_bump) = find_mint_address( - &solana_pubkey::Pubkey::new_from_array(mint_signer_1.to_bytes()), - ); - - // Derive compression addresses (from mint_signer + address_tree) - let compression_address_0 = derive_mint_compressed_address( - &solana_pubkey::Pubkey::new_from_array(mint_signer_0.to_bytes()), - &solana_pubkey::Pubkey::new_from_array(address_tree_pubkey.to_bytes()), - ); - let compression_address_1 = derive_mint_compressed_address( - &solana_pubkey::Pubkey::new_from_array(mint_signer_1.to_bytes()), - &solana_pubkey::Pubkey::new_from_array(address_tree_pubkey.to_bytes()), - ); - - // Build mint signer seeds for CPI (mint::seeds + bump) - let mint_signer_0_seeds: &[&[u8]] = &[ - MINT_SIGNER_0_SEED, - authority.as_ref(), - &[params.mint_signer_0_bump], - ]; - let mint_signer_1_seeds: &[&[u8]] = &[ - MINT_SIGNER_1_SEED, - authority.as_ref(), - &[params.mint_signer_1_bump], - ]; - - // Fixed-size array with values from accounts/attributes - let sdk_mints: [SingleMintParams<'_>; NUM_LIGHT_MINTS] = [ - SingleMintParams { - decimals: 6, // mint::decimals = 6 - address_merkle_tree_root_index: address_tree_info.root_index, - mint_authority: solana_pubkey::Pubkey::new_from_array(authority.to_bytes()), - compression_address: compression_address_0, - mint: mint_0_pda, - bump: mint_0_bump, - freeze_authority: None, - mint_seed_pubkey: solana_pubkey::Pubkey::new_from_array( - mint_signer_0.to_bytes(), - ), - authority_seeds: None, - mint_signer_seeds: Some(mint_signer_0_seeds), - token_metadata: None, - }, - SingleMintParams { - decimals: 9, // mint::decimals = 9 - address_merkle_tree_root_index: address_tree_info.root_index, - mint_authority: solana_pubkey::Pubkey::new_from_array(authority.to_bytes()), - compression_address: compression_address_1, - mint: mint_1_pda, - bump: mint_1_bump, - freeze_authority: None, - mint_seed_pubkey: solana_pubkey::Pubkey::new_from_array( - mint_signer_1.to_bytes(), - ), - authority_seeds: None, - mint_signer_seeds: Some(mint_signer_1_seeds), - token_metadata: None, - }, - ]; - - // ==================================================================== - // INVOKE invoke_create_mints - // ==================================================================== - - // Get state_tree_index (required for decompress discriminator validation) - let state_tree_index = params - .create_accounts_proof - .state_tree_index - .ok_or(LightSdkError::from(ProgramError::InvalidArgument))?; - - let proof = params - .create_accounts_proof - .proof - .0 - .ok_or(LightSdkError::from(ProgramError::InvalidArgument))?; - - let sdk_params = SdkCreateMintsParams::new(&sdk_mints, proof) - .with_output_queue_index(params.create_accounts_proof.output_state_tree_index) - .with_address_tree_index(address_tree_info.address_merkle_tree_pubkey_index) - .with_state_tree_index(state_tree_index) - .with_cpi_context_offset(NUM_LIGHT_PDAS as u8); // Offset by PDA count - - // Build infra accounts from Accounts struct - let infra = CreateMintsInfraAccounts { - fee_payer: self.payer.to_account_info(), - compressible_config: self.compressible_config.clone(), - rent_sponsor: self.rent_sponsor.clone(), - cpi_authority: self.cpi_authority.clone(), - }; - - // Build mint account arrays - let mint_seed_accounts = [ - self.mint_signer_0.to_account_info(), - self.mint_signer_1.to_account_info(), - ]; - let mint_accounts = [self.mint_0.to_account_info(), self.mint_1.to_account_info()]; - - invoke_create_mints( - &mint_seed_accounts, - &mint_accounts, - sdk_params, - infra, - &cpi_accounts, - )?; - } - Ok(WITH_CPI_CONTEXT) // false = mint-only, no CPI context write - } -} - -// ============================================================================ -// LightFinalize Implementation - No-op for mint-only flow -// ============================================================================ - -impl<'info> LightFinalize<'info, CreateDerivedMintsParams> for CreateDerivedMintsAccounts<'info> { - fn light_finalize( - &mut self, - _remaining_accounts: &[AccountInfo<'info>], - _params: &CreateDerivedMintsParams, - _has_pre_init: bool, - ) -> std::result::Result<(), LightSdkError> { - // No-op for mint-only flow - create_mints already executed in light_pre_init - Ok(()) - } -} diff --git a/sdk-tests/pinocchio-light-program-test/Cargo.toml b/sdk-tests/pinocchio-light-program-test/Cargo.toml new file mode 100644 index 0000000000..5a77352b7e --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "pinocchio-light-program-test" +version = "0.1.0" +description = "Test for #[derive(LightProgramPinocchio)] macro validation with pinocchio" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "pinocchio_light_program_test" + +[features] +no-entrypoint = [] +default = [] +test-sbf = [] + +[dependencies] +light-account-pinocchio = { workspace = true, features = ["token", "std"] } +light-macros = { workspace = true, features = ["solana"] } +light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +borsh = { workspace = true } +bytemuck = { workspace = true, features = ["derive"] } +light-compressed-account = { workspace = true, features = ["solana"] } +light-compressible = { workspace = true, features = ["pinocchio"] } +light-hasher = { workspace = true, features = ["solana"] } +light-token-types = { workspace = true } +light-token-interface = { workspace = true } +pinocchio = { workspace = true } +pinocchio-pubkey = { workspace = true } +pinocchio-system = { workspace = true } +solana-pubkey = { workspace = true } +solana-instruction = { workspace = true } +solana-msg = { workspace = true } +solana-program-error = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +light-client = { workspace = true, features = ["v2", "anchor"] } +light-test-utils = { workspace = true } +light-token = { workspace = true, features = ["anchor"] } +light-token-client = { workspace = true } +light-account = { workspace = true } +light-batched-merkle-tree = { workspace = true } +tokio = { workspace = true } +solana-sdk = { workspace = true } +solana-account = { workspace = true } +solana-keypair = { workspace = true } +solana-signer = { workspace = true } + +[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/pinocchio-light-program-test/src/account_loader/accounts.rs b/sdk-tests/pinocchio-light-program-test/src/account_loader/accounts.rs new file mode 100644 index 0000000000..d94a9ed806 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/account_loader/accounts.rs @@ -0,0 +1,87 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{CreateAccountsProof, LightAccount}; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, + sysvars::Sysvar, +}; + +use crate::state::ZeroCopyRecord; + +#[derive(BorshSerialize, BorshDeserialize, Clone)] +pub struct CreateZeroCopyRecordParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: [u8; 32], +} + +pub struct CreateZeroCopyRecord<'a> { + pub fee_payer: &'a AccountInfo, + pub compression_config: &'a AccountInfo, + pub pda_rent_sponsor: &'a AccountInfo, + pub record: &'a AccountInfo, + pub system_program: &'a AccountInfo, +} + +impl<'a> CreateZeroCopyRecord<'a> { + pub const FIXED_LEN: usize = 5; + + pub fn parse( + accounts: &'a [AccountInfo], + params: &CreateZeroCopyRecordParams, + ) -> Result { + let fee_payer = &accounts[0]; + let compression_config = &accounts[1]; + let pda_rent_sponsor = &accounts[2]; + let record = &accounts[3]; + let system_program = &accounts[4]; + + if !fee_payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + let space = 8 + ZeroCopyRecord::INIT_SPACE; + let seeds: &[&[u8]] = &[crate::RECORD_SEED, ¶ms.owner]; + let (expected_pda, bump) = pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if record.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + + let rent = + pinocchio::sysvars::rent::Rent::get().map_err(|_| ProgramError::UnsupportedSysvar)?; + let lamports = rent.minimum_balance(space); + + let bump_bytes = [bump]; + let seed_array = [ + Seed::from(crate::RECORD_SEED), + Seed::from(params.owner.as_ref()), + Seed::from(bump_bytes.as_ref()), + ]; + let signer = Signer::from(&seed_array); + pinocchio_system::instructions::CreateAccount { + from: fee_payer, + to: record, + lamports, + space: space as u64, + owner: &crate::ID, + } + .invoke_signed(&[signer])?; + + // Write LIGHT_DISCRIMINATOR + { + use light_account_pinocchio::LightDiscriminator; + let mut data = record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + data[..8].copy_from_slice(&ZeroCopyRecord::LIGHT_DISCRIMINATOR); + } + + Ok(Self { + fee_payer, + compression_config, + pda_rent_sponsor, + record, + system_program, + }) + } +} diff --git a/sdk-tests/pinocchio-light-program-test/src/account_loader/mod.rs b/sdk-tests/pinocchio-light-program-test/src/account_loader/mod.rs new file mode 100644 index 0000000000..c33d77f1e1 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/account_loader/mod.rs @@ -0,0 +1,4 @@ +pub mod accounts; +pub mod processor; + +pub use accounts::*; diff --git a/sdk-tests/pinocchio-light-program-test/src/account_loader/processor.rs b/sdk-tests/pinocchio-light-program-test/src/account_loader/processor.rs new file mode 100644 index 0000000000..9d5798d63a --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/account_loader/processor.rs @@ -0,0 +1,89 @@ +use light_account_pinocchio::{ + prepare_compressed_account_on_init, CpiAccounts, CpiAccountsConfig, InvokeLightSystemProgram, + LightAccount, LightConfig, LightSdkTypesError, PackedAddressTreeInfoExt, +}; +use light_compressed_account::instruction_data::{ + cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, +}; +use pinocchio::{ + account_info::AccountInfo, + sysvars::{clock::Clock, Sysvar}, +}; + +use super::accounts::{CreateZeroCopyRecord, CreateZeroCopyRecordParams}; +use crate::state::ZeroCopyRecord; + +pub fn process( + ctx: &CreateZeroCopyRecord<'_>, + params: &CreateZeroCopyRecordParams, + remaining_accounts: &[AccountInfo], +) -> Result<(), LightSdkTypesError> { + let system_accounts_offset = params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + ctx.fee_payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&cpi_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let output_tree_index = params.create_accounts_proof.output_state_tree_index; + let current_account_index: u8 = 0; + let cpi_context = CompressedCpiContext::default(); + let mut new_address_params = Vec::with_capacity(1); + let mut account_infos = Vec::with_capacity(1); + + let light_config = LightConfig::load_checked(ctx.compression_config, &crate::ID) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let current_slot = Clock::get() + .map_err(|_| LightSdkTypesError::InvalidInstructionData)? + .slot; + + let record_key = *ctx.record.key(); + prepare_compressed_account_on_init( + &record_key, + &address_tree_pubkey, + address_tree_info, + output_tree_index, + current_account_index, + &crate::ID, + &mut new_address_params, + &mut account_infos, + )?; + + // Set compression_info on the zero-copy record via bytemuck + { + let mut account_data = ctx + .record + .try_borrow_mut_data() + .map_err(|_| LightSdkTypesError::Borsh)?; + let record_bytes = &mut account_data[8..8 + core::mem::size_of::()]; + let record: &mut ZeroCopyRecord = bytemuck::from_bytes_mut(record_bytes); + record.set_decompressed(&light_config, current_slot); + } + + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: crate::LIGHT_CPI_SIGNER.bump, + invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: false, + with_transaction_hash: false, + cpi_context, + proof: params.create_accounts_proof.proof.0, + new_address_params, + account_infos, + read_only_addresses: vec![], + read_only_accounts: vec![], + }; + + instruction_data.invoke(cpi_accounts)?; + Ok(()) +} diff --git a/sdk-tests/pinocchio-light-program-test/src/all/accounts.rs b/sdk-tests/pinocchio-light-program-test/src/all/accounts.rs new file mode 100644 index 0000000000..828016092f --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/all/accounts.rs @@ -0,0 +1,193 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{CreateAccountsProof, LightAccount}; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, + sysvars::Sysvar, +}; + +use crate::state::{MinimalRecord, ZeroCopyRecord}; + +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateAllParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: [u8; 32], + pub mint_signer_bump: u8, + pub token_vault_bump: u8, +} + +pub struct CreateAllAccounts<'a> { + pub payer: &'a AccountInfo, + pub authority: &'a AccountInfo, + pub compression_config: &'a AccountInfo, + pub borsh_record: &'a AccountInfo, + pub zero_copy_record: &'a AccountInfo, + pub mint_signer: &'a AccountInfo, + pub mint: &'a AccountInfo, + pub token_vault: &'a AccountInfo, + pub vault_owner: &'a AccountInfo, + pub ata_owner: &'a AccountInfo, + pub user_ata: &'a AccountInfo, + pub compressible_config: &'a AccountInfo, + pub rent_sponsor: &'a AccountInfo, + pub light_token_program: &'a AccountInfo, + pub cpi_authority: &'a AccountInfo, + pub system_program: &'a AccountInfo, + pub mint_signers_slice: &'a [AccountInfo], + pub mints_slice: &'a [AccountInfo], +} + +impl<'a> CreateAllAccounts<'a> { + pub const FIXED_LEN: usize = 16; + + pub fn parse( + accounts: &'a [AccountInfo], + params: &CreateAllParams, + ) -> Result { + let payer = &accounts[0]; + let authority = &accounts[1]; + let compression_config = &accounts[2]; + let borsh_record = &accounts[3]; + let zero_copy_record = &accounts[4]; + let mint_signer = &accounts[5]; + let mint = &accounts[6]; + let token_vault = &accounts[7]; + let vault_owner = &accounts[8]; + let ata_owner = &accounts[9]; + let user_ata = &accounts[10]; + let compressible_config = &accounts[11]; + let rent_sponsor = &accounts[12]; + let light_token_program = &accounts[13]; + let cpi_authority = &accounts[14]; + let system_program = &accounts[15]; + + if !payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + if !authority.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Create Borsh PDA + { + let space = 8 + MinimalRecord::INIT_SPACE; + let seeds: &[&[u8]] = &[b"minimal_record", ¶ms.owner]; + let (expected_pda, bump) = pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if borsh_record.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + + let rent = pinocchio::sysvars::rent::Rent::get() + .map_err(|_| ProgramError::UnsupportedSysvar)?; + let lamports = rent.minimum_balance(space); + + let bump_bytes = [bump]; + let seed_array = [ + Seed::from(b"minimal_record" as &[u8]), + Seed::from(params.owner.as_ref()), + Seed::from(bump_bytes.as_ref()), + ]; + let signer = Signer::from(&seed_array); + pinocchio_system::instructions::CreateAccount { + from: payer, + to: borsh_record, + lamports, + space: space as u64, + owner: &crate::ID, + } + .invoke_signed(&[signer])?; + + use light_account_pinocchio::LightDiscriminator; + let mut data = borsh_record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + data[..8].copy_from_slice(&MinimalRecord::LIGHT_DISCRIMINATOR); + } + + // Create ZeroCopy PDA + { + let space = 8 + ZeroCopyRecord::INIT_SPACE; + let seeds: &[&[u8]] = &[crate::RECORD_SEED, ¶ms.owner]; + let (expected_pda, bump) = pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if zero_copy_record.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + + let rent = pinocchio::sysvars::rent::Rent::get() + .map_err(|_| ProgramError::UnsupportedSysvar)?; + let lamports = rent.minimum_balance(space); + + let bump_bytes = [bump]; + let seed_array = [ + Seed::from(crate::RECORD_SEED), + Seed::from(params.owner.as_ref()), + Seed::from(bump_bytes.as_ref()), + ]; + let signer = Signer::from(&seed_array); + pinocchio_system::instructions::CreateAccount { + from: payer, + to: zero_copy_record, + lamports, + space: space as u64, + owner: &crate::ID, + } + .invoke_signed(&[signer])?; + + use light_account_pinocchio::LightDiscriminator; + let mut data = zero_copy_record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + data[..8].copy_from_slice(&ZeroCopyRecord::LIGHT_DISCRIMINATOR); + } + + // Validate mint_signer PDA + { + let authority_key = authority.key(); + let seeds: &[&[u8]] = &[crate::MINT_SIGNER_SEED_A, authority_key]; + let (expected_pda, expected_bump) = + pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if mint_signer.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + if expected_bump != params.mint_signer_bump { + return Err(ProgramError::InvalidSeeds); + } + } + + // Validate token_vault PDA + { + let mint_key = mint.key(); + let seeds: &[&[u8]] = &[crate::VAULT_SEED, mint_key]; + let (expected_pda, expected_bump) = + pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if token_vault.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + if expected_bump != params.token_vault_bump { + return Err(ProgramError::InvalidSeeds); + } + } + + Ok(Self { + payer, + authority, + compression_config, + borsh_record, + zero_copy_record, + mint_signer, + mint, + token_vault, + vault_owner, + ata_owner, + user_ata, + compressible_config, + rent_sponsor, + light_token_program, + cpi_authority, + system_program, + mint_signers_slice: &accounts[5..6], + mints_slice: &accounts[6..7], + }) + } +} diff --git a/sdk-tests/pinocchio-light-program-test/src/all/mod.rs b/sdk-tests/pinocchio-light-program-test/src/all/mod.rs new file mode 100644 index 0000000000..c33d77f1e1 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/all/mod.rs @@ -0,0 +1,4 @@ +pub mod accounts; +pub mod processor; + +pub use accounts::*; diff --git a/sdk-tests/pinocchio-light-program-test/src/all/processor.rs b/sdk-tests/pinocchio-light-program-test/src/all/processor.rs new file mode 100644 index 0000000000..2fb78fabc0 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/all/processor.rs @@ -0,0 +1,215 @@ +use light_account_pinocchio::{ + derive_associated_token_account, prepare_compressed_account_on_init, CpiAccounts, + CpiAccountsConfig, CpiContextWriteAccounts, CreateMints, CreateMintsStaticAccounts, + CreateTokenAccountCpi, CreateTokenAtaCpi, InvokeLightSystemProgram, LightAccount, LightConfig, + LightSdkTypesError, PackedAddressTreeInfoExt, SingleMintParams, +}; +use light_compressed_account::instruction_data::{ + cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, +}; +use pinocchio::{ + account_info::AccountInfo, + sysvars::{clock::Clock, Sysvar}, +}; + +use super::accounts::{CreateAllAccounts, CreateAllParams}; + +pub fn process( + ctx: &CreateAllAccounts<'_>, + params: &CreateAllParams, + remaining_accounts: &[AccountInfo], +) -> Result<(), LightSdkTypesError> { + use borsh::BorshDeserialize; + + const NUM_LIGHT_PDAS: usize = 2; + const NUM_LIGHT_MINTS: usize = 1; + const WITH_CPI_CONTEXT: bool = true; + + // 1. Build CPI accounts + let system_accounts_offset = params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + ctx.payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + // 2. Address tree info + let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&cpi_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let output_tree_index = params.create_accounts_proof.output_state_tree_index; + + // 3. Load config, get slot + let light_config = LightConfig::load_checked(ctx.compression_config, &crate::ID) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let current_slot = Clock::get() + .map_err(|_| LightSdkTypesError::InvalidInstructionData)? + .slot; + + // 4. Create PDAs via invoke_write_to_cpi_context_first + { + let cpi_context = CompressedCpiContext::first(); + let mut new_address_params = Vec::with_capacity(NUM_LIGHT_PDAS); + let mut account_infos = Vec::with_capacity(NUM_LIGHT_PDAS); + + // 4a. Borsh PDA (index 0) + let borsh_record_key = *ctx.borsh_record.key(); + prepare_compressed_account_on_init( + &borsh_record_key, + &address_tree_pubkey, + address_tree_info, + output_tree_index, + 0, + &crate::ID, + &mut new_address_params, + &mut account_infos, + )?; + { + let mut account_data = ctx + .borsh_record + .try_borrow_mut_data() + .map_err(|_| LightSdkTypesError::Borsh)?; + let mut record = crate::state::MinimalRecord::try_from_slice(&account_data[8..]) + .map_err(|_| LightSdkTypesError::Borsh)?; + record.set_decompressed(&light_config, current_slot); + let serialized = borsh::to_vec(&record).map_err(|_| LightSdkTypesError::Borsh)?; + account_data[8..8 + serialized.len()].copy_from_slice(&serialized); + } + + // 4b. ZeroCopy PDA (index 1) + let zero_copy_record_key = *ctx.zero_copy_record.key(); + prepare_compressed_account_on_init( + &zero_copy_record_key, + &address_tree_pubkey, + address_tree_info, + output_tree_index, + 1, + &crate::ID, + &mut new_address_params, + &mut account_infos, + )?; + { + let mut account_data = ctx + .zero_copy_record + .try_borrow_mut_data() + .map_err(|_| LightSdkTypesError::Borsh)?; + let record_bytes = + &mut account_data[8..8 + core::mem::size_of::()]; + let record: &mut crate::state::ZeroCopyRecord = bytemuck::from_bytes_mut(record_bytes); + record.set_decompressed(&light_config, current_slot); + } + + // 4c. Write to CPI context + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: crate::LIGHT_CPI_SIGNER.bump, + invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: WITH_CPI_CONTEXT, + with_transaction_hash: false, + cpi_context, + proof: params.create_accounts_proof.proof.0, + new_address_params, + account_infos, + read_only_addresses: vec![], + read_only_accounts: vec![], + }; + + let cpi_context_accounts = CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority()?, + cpi_context: cpi_accounts.cpi_context()?, + cpi_signer: crate::LIGHT_CPI_SIGNER, + }; + instruction_data.invoke_write_to_cpi_context_first(cpi_context_accounts)?; + } + + // 5. Create Mint + { + let authority_key = *ctx.authority.key(); + let mint_signer_key = *ctx.mint_signer.key(); + + let mint_signer_seeds: &[&[u8]] = &[ + crate::MINT_SIGNER_SEED_A, + authority_key.as_ref(), + &[params.mint_signer_bump], + ]; + + let sdk_mints: [SingleMintParams<'_>; NUM_LIGHT_MINTS] = [SingleMintParams { + decimals: 9, + mint_authority: authority_key, + mint_bump: None, + freeze_authority: None, + mint_seed_pubkey: mint_signer_key, + authority_seeds: None, + mint_signer_seeds: Some(mint_signer_seeds), + token_metadata: None, + }]; + + CreateMints { + mints: &sdk_mints, + proof_data: ¶ms.create_accounts_proof, + mint_seed_accounts: ctx.mint_signers_slice, + mint_accounts: ctx.mints_slice, + static_accounts: CreateMintsStaticAccounts { + fee_payer: ctx.payer, + compressible_config: ctx.compressible_config, + rent_sponsor: ctx.rent_sponsor, + cpi_authority: ctx.cpi_authority, + }, + cpi_context_offset: NUM_LIGHT_PDAS as u8, + } + .invoke(&cpi_accounts)?; + } + + // 6. Create Token Vault + { + let mint_key = *ctx.mint.key(); + let vault_seeds: &[&[u8]] = &[ + crate::VAULT_SEED, + mint_key.as_ref(), + &[params.token_vault_bump], + ]; + + CreateTokenAccountCpi { + payer: ctx.payer, + account: ctx.token_vault, + mint: ctx.mint, + owner: *ctx.vault_owner.key(), + } + .rent_free( + ctx.compressible_config, + ctx.rent_sponsor, + ctx.system_program, + &crate::ID, + ) + .invoke_signed(vault_seeds)?; + } + + // 7. Create ATA + { + let (_, ata_bump) = derive_associated_token_account(ctx.ata_owner.key(), ctx.mint.key()); + + CreateTokenAtaCpi { + payer: ctx.payer, + owner: ctx.ata_owner, + mint: ctx.mint, + ata: ctx.user_ata, + bump: ata_bump, + } + .rent_free( + ctx.compressible_config, + ctx.rent_sponsor, + ctx.system_program, + ) + .invoke()?; + } + + Ok(()) +} diff --git a/sdk-tests/pinocchio-light-program-test/src/ata/accounts.rs b/sdk-tests/pinocchio-light-program-test/src/ata/accounts.rs new file mode 100644 index 0000000000..6f51536470 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/ata/accounts.rs @@ -0,0 +1,46 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, Default)] +pub struct CreateAtaParams {} + +pub struct CreateAtaAccounts<'a> { + pub payer: &'a AccountInfo, + pub mint: &'a AccountInfo, + pub ata_owner: &'a AccountInfo, + pub user_ata: &'a AccountInfo, + pub compressible_config: &'a AccountInfo, + pub rent_sponsor: &'a AccountInfo, + pub light_token_program: &'a AccountInfo, + pub system_program: &'a AccountInfo, +} + +impl<'a> CreateAtaAccounts<'a> { + pub const FIXED_LEN: usize = 8; + + pub fn parse(accounts: &'a [AccountInfo]) -> Result { + let payer = &accounts[0]; + let mint = &accounts[1]; + let ata_owner = &accounts[2]; + let user_ata = &accounts[3]; + let compressible_config = &accounts[4]; + let rent_sponsor = &accounts[5]; + let light_token_program = &accounts[6]; + let system_program = &accounts[7]; + + if !payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + Ok(Self { + payer, + mint, + ata_owner, + user_ata, + compressible_config, + rent_sponsor, + light_token_program, + system_program, + }) + } +} diff --git a/sdk-tests/pinocchio-light-program-test/src/ata/mod.rs b/sdk-tests/pinocchio-light-program-test/src/ata/mod.rs new file mode 100644 index 0000000000..c33d77f1e1 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/ata/mod.rs @@ -0,0 +1,4 @@ +pub mod accounts; +pub mod processor; + +pub use accounts::*; diff --git a/sdk-tests/pinocchio-light-program-test/src/ata/processor.rs b/sdk-tests/pinocchio-light-program-test/src/ata/processor.rs new file mode 100644 index 0000000000..d00a03c354 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/ata/processor.rs @@ -0,0 +1,31 @@ +use light_account_pinocchio::{ + derive_associated_token_account, CreateTokenAtaCpi, LightSdkTypesError, +}; +use pinocchio::account_info::AccountInfo; + +use super::accounts::{CreateAtaAccounts, CreateAtaParams}; + +pub fn process( + ctx: &CreateAtaAccounts<'_>, + _params: &CreateAtaParams, + _remaining_accounts: &[AccountInfo], +) -> Result<(), LightSdkTypesError> { + let (_, bump) = derive_associated_token_account(ctx.ata_owner.key(), ctx.mint.key()); + + CreateTokenAtaCpi { + payer: ctx.payer, + owner: ctx.ata_owner, + mint: ctx.mint, + ata: ctx.user_ata, + bump, + } + .idempotent() + .rent_free( + ctx.compressible_config, + ctx.rent_sponsor, + ctx.system_program, + ) + .invoke()?; + + Ok(()) +} diff --git a/sdk-tests/pinocchio-light-program-test/src/lib.rs b/sdk-tests/pinocchio-light-program-test/src/lib.rs new file mode 100644 index 0000000000..898f8776b1 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/lib.rs @@ -0,0 +1,279 @@ +//! Test program for #[derive(LightProgramPinocchio)] macro validation. +//! +//! Uses #[derive(LightProgramPinocchio)] to generate compress/decompress dispatch, +//! config handlers, and variant types. No Anchor dependency. + +#![allow(deprecated)] + +use light_account_pinocchio::{ + derive_light_cpi_signer, CpiSigner, LightAccount, LightProgramPinocchio, +}; +use light_macros::pubkey_array; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +pub mod account_loader; +pub mod all; +pub mod ata; +pub mod mint; +pub mod pda; +pub mod state; +pub mod token_account; +pub mod two_mints; + +pub use state::*; + +pub const ID: Pubkey = pubkey_array!("DrvPda11111111111111111111111111111111111111"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("DrvPda11111111111111111111111111111111111111"); + +pub const VAULT_AUTH_SEED: &[u8] = b"vault_auth"; +pub const VAULT_SEED: &[u8] = b"vault"; +pub const RECORD_SEED: &[u8] = b"zero_copy_record"; +pub const MINT_SIGNER_SEED_A: &[u8] = b"mint_signer_a"; +pub const MINT_SIGNER_SEED_B: &[u8] = b"mint_signer_b"; + +/// This generates: variant enums, compress/decompress dispatch, config handlers, +/// per-variant Seeds/Variant/Packed types, LightAccountVariantTrait impls, +/// size validation, seed providers, and client functions. +#[derive(LightProgramPinocchio)] +pub enum ProgramAccounts { + #[light_account(pda::seeds = [b"minimal_record", ctx.owner])] + MinimalRecord(MinimalRecord), + + #[light_account(associated_token)] + Ata, + + #[light_account(token::seeds = [VAULT_SEED, ctx.mint], token::owner_seeds = [VAULT_AUTH_SEED])] + Vault, + + #[light_account(pda::seeds = [RECORD_SEED, ctx.owner], pda::zero_copy)] + ZeroCopyRecord(ZeroCopyRecord), +} + +// ============================================================================ +// Instruction Discriminators (Anchor-compatible: sha256("global:{name}")[..8]) +// ============================================================================ +pub mod discriminators { + pub const CREATE_PDA: [u8; 8] = [220, 10, 244, 120, 183, 4, 64, 232]; + pub const CREATE_ATA: [u8; 8] = [26, 102, 168, 62, 117, 72, 168, 17]; + pub const CREATE_TOKEN_VAULT: [u8; 8] = [161, 29, 12, 45, 127, 88, 61, 49]; + pub const CREATE_ZERO_COPY_RECORD: [u8; 8] = [6, 252, 72, 240, 45, 91, 28, 6]; + pub const CREATE_MINT: [u8; 8] = [69, 44, 215, 132, 253, 214, 41, 45]; + pub const CREATE_TWO_MINTS: [u8; 8] = [222, 41, 188, 84, 174, 115, 236, 105]; + pub const CREATE_ALL: [u8; 8] = [149, 49, 144, 45, 208, 155, 177, 43]; +} + +// ============================================================================ +// Entrypoint +// ============================================================================ + +pinocchio::entrypoint!(process_instruction); + +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), ProgramError> { + if instruction_data.len() < 8 { + return Err(ProgramError::InvalidInstructionData); + } + + let (disc, data) = instruction_data.split_at(8); + let disc: [u8; 8] = disc.try_into().unwrap(); + + match disc { + discriminators::CREATE_PDA => process_create_pda(accounts, data), + discriminators::CREATE_ATA => process_create_ata(accounts, data), + discriminators::CREATE_TOKEN_VAULT => process_create_token_vault(accounts, data), + discriminators::CREATE_ZERO_COPY_RECORD => process_create_zero_copy_record(accounts, data), + discriminators::CREATE_MINT => process_create_mint(accounts, data), + discriminators::CREATE_TWO_MINTS => process_create_two_mints(accounts, data), + discriminators::CREATE_ALL => process_create_all(accounts, data), + ProgramAccounts::INITIALIZE_COMPRESSION_CONFIG => { + ProgramAccounts::process_initialize_config(accounts, data) + } + ProgramAccounts::UPDATE_COMPRESSION_CONFIG => { + ProgramAccounts::process_update_config(accounts, data) + } + ProgramAccounts::COMPRESS_ACCOUNTS_IDEMPOTENT => { + ProgramAccounts::process_compress(accounts, data) + } + ProgramAccounts::DECOMPRESS_ACCOUNTS_IDEMPOTENT => { + ProgramAccounts::process_decompress(accounts, data) + } + _ => Err(ProgramError::InvalidInstructionData), + } +} + +// ============================================================================ +// Instruction Handlers +// ============================================================================ + +fn process_create_pda(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use borsh::BorshDeserialize; + use pda::accounts::{CreatePda, CreatePdaParams}; + + let params = + CreatePdaParams::deserialize(&mut &data[..]).map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreatePda::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let ctx = CreatePda::parse(fixed_accounts, ¶ms)?; + + // Business logic: set account data + { + let mut account_data = ctx + .record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + let mut record = state::MinimalRecord::try_from_slice(&account_data[8..]) + .map_err(|_| ProgramError::BorshIoError)?; + record.owner = params.owner; + let serialized = borsh::to_vec(&record).map_err(|_| ProgramError::BorshIoError)?; + account_data[8..8 + serialized.len()].copy_from_slice(&serialized); + } + + pda::processor::process(&ctx, ¶ms, remaining_accounts) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_ata(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use ata::accounts::{CreateAtaAccounts, CreateAtaParams}; + use borsh::BorshDeserialize; + + let params = + CreateAtaParams::deserialize(&mut &data[..]).map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateAtaAccounts::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let ctx = CreateAtaAccounts::parse(fixed_accounts)?; + + ata::processor::process(&ctx, ¶ms, remaining_accounts) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_token_vault(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use borsh::BorshDeserialize; + use token_account::accounts::{CreateTokenVaultAccounts, CreateTokenVaultParams}; + + let params = CreateTokenVaultParams::deserialize(&mut &data[..]) + .map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateTokenVaultAccounts::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let ctx = CreateTokenVaultAccounts::parse(fixed_accounts)?; + + token_account::processor::process(&ctx, ¶ms, remaining_accounts) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_zero_copy_record( + accounts: &[AccountInfo], + data: &[u8], +) -> Result<(), ProgramError> { + use account_loader::accounts::{CreateZeroCopyRecord, CreateZeroCopyRecordParams}; + use borsh::BorshDeserialize; + + let params = CreateZeroCopyRecordParams::deserialize(&mut &data[..]) + .map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateZeroCopyRecord::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let ctx = CreateZeroCopyRecord::parse(fixed_accounts, ¶ms)?; + + // Business logic: set zero-copy account data + { + let mut account_data = ctx + .record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + let record_bytes = &mut account_data[8..8 + core::mem::size_of::()]; + let record: &mut state::ZeroCopyRecord = bytemuck::from_bytes_mut(record_bytes); + record.owner = params.owner; + } + + account_loader::processor::process(&ctx, ¶ms, remaining_accounts) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_mint(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use borsh::BorshDeserialize; + use mint::accounts::{CreateMintAccounts, CreateMintParams}; + + let params = + CreateMintParams::deserialize(&mut &data[..]).map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateMintAccounts::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let ctx = CreateMintAccounts::parse(fixed_accounts, ¶ms)?; + + mint::processor::process(&ctx, ¶ms, remaining_accounts) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_two_mints(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use borsh::BorshDeserialize; + use two_mints::accounts::{CreateTwoMintsAccounts, CreateTwoMintsParams}; + + let params = CreateTwoMintsParams::deserialize(&mut &data[..]) + .map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateTwoMintsAccounts::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let ctx = CreateTwoMintsAccounts::parse(fixed_accounts, ¶ms)?; + + two_mints::processor::process(&ctx, ¶ms, remaining_accounts) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_all(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use all::accounts::{CreateAllAccounts, CreateAllParams}; + use borsh::BorshDeserialize; + + let params = + CreateAllParams::deserialize(&mut &data[..]).map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateAllAccounts::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let ctx = CreateAllAccounts::parse(fixed_accounts, ¶ms)?; + + // Business logic: set PDA data + { + let mut borsh_data = ctx + .borsh_record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + let mut borsh_record = state::MinimalRecord::try_from_slice(&borsh_data[8..]) + .map_err(|_| ProgramError::BorshIoError)?; + borsh_record.owner = params.owner; + let serialized = borsh::to_vec(&borsh_record).map_err(|_| ProgramError::BorshIoError)?; + borsh_data[8..8 + serialized.len()].copy_from_slice(&serialized); + } + { + let mut zc_data = ctx + .zero_copy_record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + let record_bytes = &mut zc_data[8..8 + core::mem::size_of::()]; + let record: &mut state::ZeroCopyRecord = bytemuck::from_bytes_mut(record_bytes); + record.owner = params.owner; + } + + all::processor::process(&ctx, ¶ms, remaining_accounts) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} diff --git a/sdk-tests/pinocchio-light-program-test/src/mint/accounts.rs b/sdk-tests/pinocchio-light-program-test/src/mint/accounts.rs new file mode 100644 index 0000000000..8f524b0a55 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/mint/accounts.rs @@ -0,0 +1,73 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::CreateAccountsProof; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateMintParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_bump: u8, +} + +pub struct CreateMintAccounts<'a> { + pub payer: &'a AccountInfo, + pub authority: &'a AccountInfo, + pub mint_signer: &'a AccountInfo, + pub mint: &'a AccountInfo, + pub compressible_config: &'a AccountInfo, + pub rent_sponsor: &'a AccountInfo, + pub light_token_program: &'a AccountInfo, + pub cpi_authority: &'a AccountInfo, + pub system_program: &'a AccountInfo, +} + +impl<'a> CreateMintAccounts<'a> { + pub const FIXED_LEN: usize = 9; + + pub fn parse( + accounts: &'a [AccountInfo], + params: &CreateMintParams, + ) -> Result { + let payer = &accounts[0]; + let authority = &accounts[1]; + let mint_signer = &accounts[2]; + let mint = &accounts[3]; + let compressible_config = &accounts[4]; + let rent_sponsor = &accounts[5]; + let light_token_program = &accounts[6]; + let cpi_authority = &accounts[7]; + let system_program = &accounts[8]; + + if !payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + if !authority.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Validate mint_signer PDA + { + let authority_key = authority.key(); + let seeds: &[&[u8]] = &[crate::MINT_SIGNER_SEED_A, authority_key]; + let (expected_pda, expected_bump) = + pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if mint_signer.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + if expected_bump != params.mint_signer_bump { + return Err(ProgramError::InvalidSeeds); + } + } + + Ok(Self { + payer, + authority, + mint_signer, + mint, + compressible_config, + rent_sponsor, + light_token_program, + cpi_authority, + system_program, + }) + } +} diff --git a/sdk-tests/pinocchio-light-program-test/src/mint/mod.rs b/sdk-tests/pinocchio-light-program-test/src/mint/mod.rs new file mode 100644 index 0000000000..c33d77f1e1 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/mint/mod.rs @@ -0,0 +1,4 @@ +pub mod accounts; +pub mod processor; + +pub use accounts::*; diff --git a/sdk-tests/pinocchio-light-program-test/src/mint/processor.rs b/sdk-tests/pinocchio-light-program-test/src/mint/processor.rs new file mode 100644 index 0000000000..bbdef81a1c --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/mint/processor.rs @@ -0,0 +1,62 @@ +use light_account_pinocchio::{ + CpiAccounts, CpiAccountsConfig, CreateMints, CreateMintsStaticAccounts, LightSdkTypesError, + SingleMintParams, +}; +use pinocchio::account_info::AccountInfo; + +use super::accounts::{CreateMintAccounts, CreateMintParams}; + +pub fn process( + ctx: &CreateMintAccounts<'_>, + params: &CreateMintParams, + remaining_accounts: &[AccountInfo], +) -> Result<(), LightSdkTypesError> { + let system_accounts_offset = params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + ctx.payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + let authority = *ctx.authority.key(); + let mint_signer_key = *ctx.mint_signer.key(); + + let mint_signer_seeds: &[&[u8]] = &[ + crate::MINT_SIGNER_SEED_A, + authority.as_ref(), + &[params.mint_signer_bump], + ]; + + let sdk_mints: [SingleMintParams<'_>; 1] = [SingleMintParams { + decimals: 9, + mint_authority: authority, + mint_bump: None, + freeze_authority: None, + mint_seed_pubkey: mint_signer_key, + authority_seeds: None, + mint_signer_seeds: Some(mint_signer_seeds), + token_metadata: None, + }]; + + let mint_signers = core::slice::from_ref(ctx.mint_signer); + let mints = core::slice::from_ref(ctx.mint); + + CreateMints { + mints: &sdk_mints, + proof_data: ¶ms.create_accounts_proof, + mint_seed_accounts: mint_signers, + mint_accounts: mints, + static_accounts: CreateMintsStaticAccounts { + fee_payer: ctx.payer, + compressible_config: ctx.compressible_config, + rent_sponsor: ctx.rent_sponsor, + cpi_authority: ctx.cpi_authority, + }, + cpi_context_offset: 0, + } + .invoke(&cpi_accounts) +} diff --git a/sdk-tests/pinocchio-light-program-test/src/pda/accounts.rs b/sdk-tests/pinocchio-light-program-test/src/pda/accounts.rs new file mode 100644 index 0000000000..1285b7c676 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/pda/accounts.rs @@ -0,0 +1,88 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{CreateAccountsProof, LightAccount}; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, + sysvars::Sysvar, +}; + +use crate::state::MinimalRecord; + +#[derive(BorshSerialize, BorshDeserialize, Clone)] +pub struct CreatePdaParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: [u8; 32], +} + +pub struct CreatePda<'a> { + pub fee_payer: &'a AccountInfo, + pub compression_config: &'a AccountInfo, + pub pda_rent_sponsor: &'a AccountInfo, + pub record: &'a AccountInfo, + pub system_program: &'a AccountInfo, +} + +impl<'a> CreatePda<'a> { + pub const FIXED_LEN: usize = 5; + + pub fn parse( + accounts: &'a [AccountInfo], + params: &CreatePdaParams, + ) -> Result { + let fee_payer = &accounts[0]; + let compression_config = &accounts[1]; + let pda_rent_sponsor = &accounts[2]; + let record = &accounts[3]; + let system_program = &accounts[4]; + + if !fee_payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Derive PDA and create account + let space = 8 + MinimalRecord::INIT_SPACE; + let seeds: &[&[u8]] = &[b"minimal_record", ¶ms.owner]; + let (expected_pda, bump) = pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if record.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + + let rent = + pinocchio::sysvars::rent::Rent::get().map_err(|_| ProgramError::UnsupportedSysvar)?; + let lamports = rent.minimum_balance(space); + + let bump_bytes = [bump]; + let seed_array = [ + Seed::from(b"minimal_record" as &[u8]), + Seed::from(params.owner.as_ref()), + Seed::from(bump_bytes.as_ref()), + ]; + let signer = Signer::from(&seed_array); + pinocchio_system::instructions::CreateAccount { + from: fee_payer, + to: record, + lamports, + space: space as u64, + owner: &crate::ID, + } + .invoke_signed(&[signer])?; + + // Write LIGHT_DISCRIMINATOR to first 8 bytes + { + use light_account_pinocchio::LightDiscriminator; + let mut data = record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + data[..8].copy_from_slice(&MinimalRecord::LIGHT_DISCRIMINATOR); + } + + Ok(Self { + fee_payer, + compression_config, + pda_rent_sponsor, + record, + system_program, + }) + } +} diff --git a/sdk-tests/pinocchio-light-program-test/src/pda/mod.rs b/sdk-tests/pinocchio-light-program-test/src/pda/mod.rs new file mode 100644 index 0000000000..c33d77f1e1 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/pda/mod.rs @@ -0,0 +1,4 @@ +pub mod accounts; +pub mod processor; + +pub use accounts::*; diff --git a/sdk-tests/pinocchio-light-program-test/src/pda/processor.rs b/sdk-tests/pinocchio-light-program-test/src/pda/processor.rs new file mode 100644 index 0000000000..64746a953c --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/pda/processor.rs @@ -0,0 +1,91 @@ +use light_account_pinocchio::{ + prepare_compressed_account_on_init, CpiAccounts, CpiAccountsConfig, InvokeLightSystemProgram, + LightAccount, LightConfig, LightSdkTypesError, PackedAddressTreeInfoExt, +}; +use light_compressed_account::instruction_data::{ + cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, +}; +use pinocchio::{ + account_info::AccountInfo, + sysvars::{clock::Clock, Sysvar}, +}; + +use super::accounts::{CreatePda, CreatePdaParams}; + +pub fn process( + ctx: &CreatePda<'_>, + params: &CreatePdaParams, + remaining_accounts: &[AccountInfo], +) -> Result<(), LightSdkTypesError> { + let system_accounts_offset = params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + ctx.fee_payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&cpi_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let output_tree_index = params.create_accounts_proof.output_state_tree_index; + let current_account_index: u8 = 0; + let cpi_context = CompressedCpiContext::default(); + let mut new_address_params = Vec::with_capacity(1); + let mut account_infos = Vec::with_capacity(1); + + let light_config = LightConfig::load_checked(ctx.compression_config, &crate::ID) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let current_slot = Clock::get() + .map_err(|_| LightSdkTypesError::InvalidInstructionData)? + .slot; + + let record_key = *ctx.record.key(); + prepare_compressed_account_on_init( + &record_key, + &address_tree_pubkey, + address_tree_info, + output_tree_index, + current_account_index, + &crate::ID, + &mut new_address_params, + &mut account_infos, + )?; + + // Set compression_info on the record via borsh deserialize/serialize + { + use borsh::BorshDeserialize; + let mut account_data = ctx + .record + .try_borrow_mut_data() + .map_err(|_| LightSdkTypesError::Borsh)?; + let mut record = crate::state::MinimalRecord::try_from_slice(&account_data[8..]) + .map_err(|_| LightSdkTypesError::Borsh)?; + record.set_decompressed(&light_config, current_slot); + let serialized = borsh::to_vec(&record).map_err(|_| LightSdkTypesError::Borsh)?; + account_data[8..8 + serialized.len()].copy_from_slice(&serialized); + } + + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: crate::LIGHT_CPI_SIGNER.bump, + invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: false, + with_transaction_hash: false, + cpi_context, + proof: params.create_accounts_proof.proof.0, + new_address_params, + account_infos, + read_only_addresses: vec![], + read_only_accounts: vec![], + }; + + instruction_data.invoke(cpi_accounts)?; + Ok(()) +} diff --git a/sdk-tests/pinocchio-light-program-test/src/state.rs b/sdk-tests/pinocchio-light-program-test/src/state.rs new file mode 100644 index 0000000000..ac696b1eff --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/state.rs @@ -0,0 +1,46 @@ +//! State module for pinocchio-light-program-test. +//! +//! Pinocchio-compatible account types using BorshSerialize/BorshDeserialize +//! instead of Anchor's #[account] macro. + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{CompressionInfo, LightDiscriminator, LightPinocchioAccount}; +use pinocchio::pubkey::Pubkey; + +/// Minimal record struct for testing PDA creation. +/// Contains compression_info and one field. +/// +/// LightPinocchioAccount generates: +/// - LightHasherSha (DataHasher + ToByteArray) +/// - LightDiscriminator +/// - LightAccount trait impl with pack/unpack +/// - PackedMinimalRecord struct +#[derive( + Default, Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize, LightPinocchioAccount, +)] +#[repr(C)] +pub struct MinimalRecord { + pub compression_info: CompressionInfo, + pub owner: Pubkey, +} + +/// A zero-copy account using Pod serialization. +/// Used for efficient on-chain zero-copy access. +#[derive( + Default, + Debug, + Copy, + Clone, + PartialEq, + BorshSerialize, + BorshDeserialize, + LightPinocchioAccount, + bytemuck::Pod, + bytemuck::Zeroable, +)] +#[repr(C)] +pub struct ZeroCopyRecord { + pub compression_info: CompressionInfo, + pub owner: Pubkey, + pub counter: u64, +} diff --git a/sdk-tests/pinocchio-light-program-test/src/token_account/accounts.rs b/sdk-tests/pinocchio-light-program-test/src/token_account/accounts.rs new file mode 100644 index 0000000000..5ef226de06 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/token_account/accounts.rs @@ -0,0 +1,58 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateTokenVaultParams { + pub vault_bump: u8, +} + +pub struct CreateTokenVaultAccounts<'a> { + pub payer: &'a AccountInfo, + pub mint: &'a AccountInfo, + pub vault_owner: &'a AccountInfo, + pub token_vault: &'a AccountInfo, + pub compressible_config: &'a AccountInfo, + pub rent_sponsor: &'a AccountInfo, + pub light_token_program: &'a AccountInfo, + pub system_program: &'a AccountInfo, +} + +impl<'a> CreateTokenVaultAccounts<'a> { + pub const FIXED_LEN: usize = 8; + + pub fn parse(accounts: &'a [AccountInfo]) -> Result { + let payer = &accounts[0]; + let mint = &accounts[1]; + let vault_owner = &accounts[2]; + let token_vault = &accounts[3]; + let compressible_config = &accounts[4]; + let rent_sponsor = &accounts[5]; + let light_token_program = &accounts[6]; + let system_program = &accounts[7]; + + if !payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Validate token_vault PDA + { + let mint_key = mint.key(); + let seeds: &[&[u8]] = &[crate::VAULT_SEED, mint_key]; + let (expected_pda, _bump) = pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if token_vault.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + } + + Ok(Self { + payer, + mint, + vault_owner, + token_vault, + compressible_config, + rent_sponsor, + light_token_program, + system_program, + }) + } +} diff --git a/sdk-tests/pinocchio-light-program-test/src/token_account/mod.rs b/sdk-tests/pinocchio-light-program-test/src/token_account/mod.rs new file mode 100644 index 0000000000..c33d77f1e1 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/token_account/mod.rs @@ -0,0 +1,4 @@ +pub mod accounts; +pub mod processor; + +pub use accounts::*; diff --git a/sdk-tests/pinocchio-light-program-test/src/token_account/processor.rs b/sdk-tests/pinocchio-light-program-test/src/token_account/processor.rs new file mode 100644 index 0000000000..fc63dfc8c8 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/token_account/processor.rs @@ -0,0 +1,29 @@ +use light_account_pinocchio::{CreateTokenAccountCpi, LightSdkTypesError}; +use pinocchio::account_info::AccountInfo; + +use super::accounts::{CreateTokenVaultAccounts, CreateTokenVaultParams}; + +pub fn process( + ctx: &CreateTokenVaultAccounts<'_>, + params: &CreateTokenVaultParams, + _remaining_accounts: &[AccountInfo], +) -> Result<(), LightSdkTypesError> { + let mint_key = *ctx.mint.key(); + let vault_seeds: &[&[u8]] = &[crate::VAULT_SEED, mint_key.as_ref(), &[params.vault_bump]]; + + CreateTokenAccountCpi { + payer: ctx.payer, + account: ctx.token_vault, + mint: ctx.mint, + owner: *ctx.vault_owner.key(), + } + .rent_free( + ctx.compressible_config, + ctx.rent_sponsor, + ctx.system_program, + &crate::ID, + ) + .invoke_signed(vault_seeds)?; + + Ok(()) +} diff --git a/sdk-tests/pinocchio-light-program-test/src/two_mints/accounts.rs b/sdk-tests/pinocchio-light-program-test/src/two_mints/accounts.rs new file mode 100644 index 0000000000..c2b925bc26 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/two_mints/accounts.rs @@ -0,0 +1,98 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::CreateAccountsProof; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateTwoMintsParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_bump_a: u8, + pub mint_signer_bump_b: u8, +} + +pub struct CreateTwoMintsAccounts<'a> { + pub payer: &'a AccountInfo, + pub authority: &'a AccountInfo, + pub mint_signer_a: &'a AccountInfo, + pub mint_signer_b: &'a AccountInfo, + pub mint_a: &'a AccountInfo, + pub mint_b: &'a AccountInfo, + pub compressible_config: &'a AccountInfo, + pub rent_sponsor: &'a AccountInfo, + pub light_token_program: &'a AccountInfo, + pub cpi_authority: &'a AccountInfo, + pub system_program: &'a AccountInfo, + pub mint_signers_slice: &'a [AccountInfo], + pub mints_slice: &'a [AccountInfo], +} + +impl<'a> CreateTwoMintsAccounts<'a> { + pub const FIXED_LEN: usize = 11; + + pub fn parse( + accounts: &'a [AccountInfo], + params: &CreateTwoMintsParams, + ) -> Result { + let payer = &accounts[0]; + let authority = &accounts[1]; + let mint_signer_a = &accounts[2]; + let mint_signer_b = &accounts[3]; + let mint_a = &accounts[4]; + let mint_b = &accounts[5]; + let compressible_config = &accounts[6]; + let rent_sponsor = &accounts[7]; + let light_token_program = &accounts[8]; + let cpi_authority = &accounts[9]; + let system_program = &accounts[10]; + + if !payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + if !authority.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Validate mint_signer_a PDA + { + let authority_key = authority.key(); + let seeds: &[&[u8]] = &[crate::MINT_SIGNER_SEED_A, authority_key]; + let (expected_pda, expected_bump) = + pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if mint_signer_a.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + if expected_bump != params.mint_signer_bump_a { + return Err(ProgramError::InvalidSeeds); + } + } + + // Validate mint_signer_b PDA + { + let authority_key = authority.key(); + let seeds: &[&[u8]] = &[crate::MINT_SIGNER_SEED_B, authority_key]; + let (expected_pda, expected_bump) = + pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if mint_signer_b.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + if expected_bump != params.mint_signer_bump_b { + return Err(ProgramError::InvalidSeeds); + } + } + + Ok(Self { + payer, + authority, + mint_signer_a, + mint_signer_b, + mint_a, + mint_b, + compressible_config, + rent_sponsor, + light_token_program, + cpi_authority, + system_program, + mint_signers_slice: &accounts[2..4], + mints_slice: &accounts[4..6], + }) + } +} diff --git a/sdk-tests/pinocchio-light-program-test/src/two_mints/mod.rs b/sdk-tests/pinocchio-light-program-test/src/two_mints/mod.rs new file mode 100644 index 0000000000..c33d77f1e1 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/two_mints/mod.rs @@ -0,0 +1,4 @@ +pub mod accounts; +pub mod processor; + +pub use accounts::*; diff --git a/sdk-tests/pinocchio-light-program-test/src/two_mints/processor.rs b/sdk-tests/pinocchio-light-program-test/src/two_mints/processor.rs new file mode 100644 index 0000000000..c7c155216f --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/src/two_mints/processor.rs @@ -0,0 +1,75 @@ +use light_account_pinocchio::{ + CpiAccounts, CpiAccountsConfig, CreateMints, CreateMintsStaticAccounts, LightSdkTypesError, + SingleMintParams, +}; +use pinocchio::account_info::AccountInfo; + +use super::accounts::{CreateTwoMintsAccounts, CreateTwoMintsParams}; + +pub fn process( + ctx: &CreateTwoMintsAccounts<'_>, + params: &CreateTwoMintsParams, + remaining_accounts: &[AccountInfo], +) -> Result<(), LightSdkTypesError> { + let system_accounts_offset = params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + ctx.payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + let authority = *ctx.authority.key(); + + let mint_signer_a_seeds: &[&[u8]] = &[ + crate::MINT_SIGNER_SEED_A, + authority.as_ref(), + &[params.mint_signer_bump_a], + ]; + let mint_signer_b_seeds: &[&[u8]] = &[ + crate::MINT_SIGNER_SEED_B, + authority.as_ref(), + &[params.mint_signer_bump_b], + ]; + + let sdk_mints: [SingleMintParams<'_>; 2] = [ + SingleMintParams { + decimals: 9, + mint_authority: authority, + mint_bump: None, + freeze_authority: None, + mint_seed_pubkey: *ctx.mint_signer_a.key(), + authority_seeds: None, + mint_signer_seeds: Some(mint_signer_a_seeds), + token_metadata: None, + }, + SingleMintParams { + decimals: 6, + mint_authority: authority, + mint_bump: None, + freeze_authority: None, + mint_seed_pubkey: *ctx.mint_signer_b.key(), + authority_seeds: None, + mint_signer_seeds: Some(mint_signer_b_seeds), + token_metadata: None, + }, + ]; + + CreateMints { + mints: &sdk_mints, + proof_data: ¶ms.create_accounts_proof, + mint_seed_accounts: ctx.mint_signers_slice, + mint_accounts: ctx.mints_slice, + static_accounts: CreateMintsStaticAccounts { + fee_payer: ctx.payer, + compressible_config: ctx.compressible_config, + rent_sponsor: ctx.rent_sponsor, + cpi_authority: ctx.cpi_authority, + }, + cpi_context_offset: 0, + } + .invoke(&cpi_accounts) +} diff --git a/sdk-tests/pinocchio-light-program-test/tests/shared/mod.rs b/sdk-tests/pinocchio-light-program-test/tests/shared/mod.rs new file mode 100644 index 0000000000..c855d4289a --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/tests/shared/mod.rs @@ -0,0 +1,221 @@ +#![allow(dead_code)] + +use light_account::derive_rent_sponsor_pda; +use light_client::interface::InitializeRentFreeConfig; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest}, + Indexer, ProgramTestConfig, Rpc, +}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Shared test environment with initialized compression config. +pub struct TestEnv { + pub rpc: LightProgramTest, + pub payer: Keypair, + pub program_id: Pubkey, + pub config_pda: Pubkey, + pub rent_sponsor: Pubkey, +} + +/// Sets up a test environment with program, config, and rent sponsor initialized. +pub async fn setup_test_env() -> TestEnv { + let program_id = Pubkey::new_from_array(pinocchio_light_program_test::ID); + let mut config = ProgramTestConfig::new_v2( + true, + Some(vec![("pinocchio_light_program_test", program_id)]), + ); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let (rent_sponsor, _) = derive_rent_sponsor_pda(&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"); + + TestEnv { + rpc, + payer, + program_id, + config_pda, + rent_sponsor, + } +} + +/// Creates a compressed mint using the ctoken SDK. +/// Returns (mint_pda, mint_seed_keypair). +pub async fn setup_create_mint( + rpc: &mut (impl Rpc + Indexer), + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, +) -> (Pubkey, Keypair) { + use light_token::instruction::{CreateMint, CreateMintParams}; + + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_seed.pubkey(), + &address_tree.tree, + ); + + let (mint, bump) = light_token::instruction::find_mint_address(&mint_seed.pubkey()); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + (mint, mint_seed) +} + +pub async fn assert_onchain_exists(rpc: &mut LightProgramTest, pda: &Pubkey, name: &str) { + assert!( + rpc.get_account(*pda).await.unwrap().is_some(), + "{} account ({}) should exist on-chain", + name, + pda + ); +} + +pub async fn assert_onchain_closed(rpc: &mut LightProgramTest, pda: &Pubkey, name: &str) { + let acc = rpc.get_account(*pda).await.unwrap(); + assert!( + acc.is_none() || acc.unwrap().lamports == 0, + "{} account ({}) should be closed", + name, + pda + ); +} + +pub async fn assert_compressed_token_exists( + rpc: &mut LightProgramTest, + owner: &Pubkey, + expected_amount: u64, + name: &str, +) { + let accs = rpc + .get_compressed_token_accounts_by_owner(owner, None, None) + .await + .unwrap() + .value + .items; + assert!( + !accs.is_empty(), + "{} compressed token account should exist for owner {}", + name, + owner + ); + assert_eq!( + accs[0].token.amount, expected_amount, + "{} token amount mismatch", + name + ); +} + +pub async fn assert_rent_sponsor_paid_for_accounts( + rpc: &mut LightProgramTest, + rent_sponsor: &Pubkey, + rent_sponsor_balance_before: u64, + created_accounts: &[Pubkey], +) { + let rent_sponsor_balance_after = rpc + .get_account(*rent_sponsor) + .await + .expect("get rent sponsor account") + .map(|a| a.lamports) + .unwrap_or(0); + + let mut total_account_lamports = 0u64; + for account in created_accounts { + let account_lamports = rpc + .get_account(*account) + .await + .expect("get created account") + .map(|a| a.lamports) + .unwrap_or(0); + total_account_lamports += account_lamports; + } + + let rent_sponsor_paid = rent_sponsor_balance_before.saturating_sub(rent_sponsor_balance_after); + + assert!( + rent_sponsor_paid >= total_account_lamports, + "Rent sponsor should have paid at least {} lamports for accounts, but only paid {}. \ + Before: {}, After: {}", + total_account_lamports, + rent_sponsor_paid, + rent_sponsor_balance_before, + rent_sponsor_balance_after + ); +} + +pub fn expected_compression_info( + actual: &light_account::CompressionInfo, +) -> light_account::CompressionInfo { + *actual +} + +pub fn parse_token(data: &[u8]) -> light_token_interface::state::token::Token { + borsh::BorshDeserialize::deserialize(&mut &data[..]).unwrap() +} + +/// Build instruction data: discriminator + borsh-serialized params. +pub fn build_instruction_data(disc: &[u8; 8], params: &T) -> Vec { + let mut data = Vec::new(); + data.extend_from_slice(disc); + borsh::BorshSerialize::serialize(params, &mut data).unwrap(); + data +} diff --git a/sdk-tests/pinocchio-light-program-test/tests/stress_test.rs b/sdk-tests/pinocchio-light-program-test/tests/stress_test.rs new file mode 100644 index 0000000000..108a1a1a92 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/tests/stress_test.rs @@ -0,0 +1,480 @@ +/// Stress test: 20-iteration compression/decompression cycles for all account types. +/// +/// Tests repeated cycles of: +/// 1. Decompress all accounts +/// 2. Assert cached state matches on-chain state +/// 3. Update cache from on-chain state +/// 4. Compress all accounts (warp forward) +mod shared; + +use light_account_pinocchio::token::TokenDataWithSeeds; +use light_batched_merkle_tree::{ + initialize_address_tree::InitAddressTreeAccountsInstructionData, + initialize_state_tree::InitStateTreeAccountsInstructionData, +}; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountInterfaceExt, + AccountSpec, ColdContext, CreateAccountsProofInput, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, + ProgramTestConfig, Rpc, +}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_token_interface::state::{token::Token, Mint}; +use pinocchio_light_program_test::{ + all::accounts::CreateAllParams, discriminators, LightAccountVariant, MinimalRecord, + MinimalRecordSeeds, VaultSeeds, ZeroCopyRecord, ZeroCopyRecordSeeds, MINT_SIGNER_SEED_A, + RECORD_SEED, VAULT_AUTH_SEED, VAULT_SEED, +}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Stores all derived PDAs +#[allow(dead_code)] +struct TestPdas { + record: Pubkey, + zc_record: Pubkey, + ata: Pubkey, + ata_owner: Pubkey, + vault: Pubkey, + vault_owner: Pubkey, + mint: Pubkey, +} + +/// Cached state for accounts that go through the compress/decompress cycle. +#[derive(Clone)] +struct CachedState { + record: MinimalRecord, + zc_record: ZeroCopyRecord, + ata_token: Token, + vault_token: Token, + owner: [u8; 32], +} + +/// Test context +struct StressTestContext { + rpc: LightProgramTest, + payer: Keypair, + config_pda: Pubkey, + program_id: Pubkey, +} + +fn parse_token(data: &[u8]) -> Token { + borsh::BorshDeserialize::deserialize(&mut &data[..]).unwrap() +} + +/// Setup environment with larger queues for stress test +async fn setup() -> (StressTestContext, TestPdas) { + let program_id = Pubkey::new_from_array(pinocchio_light_program_test::ID); + let mut config = ProgramTestConfig::new_v2( + true, + Some(vec![("pinocchio_light_program_test", program_id)]), + ) + .with_light_protocol_events(); + config.v2_state_tree_config = Some(InitStateTreeAccountsInstructionData::e2e_test_default()); + config.v2_address_tree_config = + Some(InitAddressTreeAccountsInstructionData::e2e_test_default()); + + 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 (rent_sponsor, _) = light_account::derive_rent_sponsor_pda(&program_id); + + let (init_config_ix, config_pda) = light_client::interface::InitializeRentFreeConfig::new( + &program_id, + &payer.pubkey(), + &program_data_pda, + rent_sponsor, + payer.pubkey(), + ) + .build(); + + rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + .await + .expect("Initialize config should succeed"); + + let owner = Keypair::new().pubkey(); + let authority = Keypair::new(); + + // Derive all PDAs + let (record_pda, _) = + Pubkey::find_program_address(&[b"minimal_record", owner.as_ref()], &program_id); + let (zc_record_pda, _) = + Pubkey::find_program_address(&[RECORD_SEED, owner.as_ref()], &program_id); + + // Mint signer PDA + let (mint_signer, mint_signer_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_A, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_pda, _) = light_token::instruction::find_mint_address(&mint_signer); + + // Token vault PDA (uses the mint we're creating) + let (vault_owner, _) = Pubkey::find_program_address(&[VAULT_AUTH_SEED], &program_id); + let (vault, vault_bump) = + Pubkey::find_program_address(&[VAULT_SEED, mint_pda.as_ref()], &program_id); + + // ATA (uses the mint we're creating) + let ata_owner = payer.pubkey(); + let (ata, _) = light_token::instruction::derive_token_ata(&ata_owner, &mint_pda); + + // Create all accounts in one instruction + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::pda(record_pda), + CreateAccountsProofInput::pda(zc_record_pda), + CreateAccountsProofInput::mint(mint_signer), + ], + ) + .await + .unwrap(); + + let params = CreateAllParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner: owner.to_bytes(), + mint_signer_bump, + token_vault_bump: vault_bump, + }; + + // Account order per all/accounts.rs + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(authority.pubkey(), true), + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new(record_pda, false), + AccountMeta::new(zc_record_pda, false), + AccountMeta::new_readonly(mint_signer, false), + AccountMeta::new(mint_pda, false), + AccountMeta::new(vault, false), + AccountMeta::new_readonly(vault_owner, false), + AccountMeta::new_readonly(ata_owner, false), + AccountMeta::new(ata, false), + AccountMeta::new_readonly(LIGHT_TOKEN_CONFIG, false), + AccountMeta::new(LIGHT_TOKEN_RENT_SPONSOR, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID.into(), false), + AccountMeta::new_readonly(light_token_types::CPI_AUTHORITY_PDA.into(), false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let instruction = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: shared::build_instruction_data(&discriminators::CREATE_ALL, ¶ms), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateAll should succeed"); + + let pdas = TestPdas { + record: record_pda, + zc_record: zc_record_pda, + ata, + ata_owner, + vault, + vault_owner, + mint: mint_pda, + }; + + let ctx = StressTestContext { + rpc, + payer, + config_pda, + program_id, + }; + + (ctx, pdas) +} + +/// Re-read all on-chain accounts into the cache +async fn refresh_cache( + rpc: &mut LightProgramTest, + pdas: &TestPdas, + owner: [u8; 32], +) -> CachedState { + let record_account = rpc.get_account(pdas.record).await.unwrap().unwrap(); + let record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &record_account.data[8..]).unwrap(); + + let zc_account = rpc.get_account(pdas.zc_record).await.unwrap().unwrap(); + let zc_record: ZeroCopyRecord = *bytemuck::from_bytes(&zc_account.data[8..]); + + let ata_token = parse_token(&rpc.get_account(pdas.ata).await.unwrap().unwrap().data); + let vault_token = parse_token(&rpc.get_account(pdas.vault).await.unwrap().unwrap().data); + + CachedState { + record, + zc_record, + ata_token, + vault_token, + owner, + } +} + +/// Decompress all accounts +async fn decompress_all(ctx: &mut StressTestContext, pdas: &TestPdas, cached: &CachedState) { + // PDA: MinimalRecord + let record_interface = ctx + .rpc + .get_account_interface(&pdas.record, &ctx.program_id) + .await + .expect("failed to get MinimalRecord interface"); + assert!(record_interface.is_cold(), "MinimalRecord should be cold"); + + let record_data: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &record_interface.account.data[8..]) + .expect("Failed to parse MinimalRecord"); + let record_variant = LightAccountVariant::MinimalRecord { + seeds: MinimalRecordSeeds { + owner: cached.owner, + }, + data: record_data, + }; + let record_spec = PdaSpec::new(record_interface, record_variant, ctx.program_id); + + // PDA: ZeroCopyRecord + let zc_interface = ctx + .rpc + .get_account_interface(&pdas.zc_record, &ctx.program_id) + .await + .expect("failed to get ZeroCopyRecord interface"); + assert!(zc_interface.is_cold(), "ZeroCopyRecord should be cold"); + + let zc_data: ZeroCopyRecord = + borsh::BorshDeserialize::deserialize(&mut &zc_interface.account.data[8..]) + .expect("Failed to parse ZeroCopyRecord"); + let zc_variant = LightAccountVariant::ZeroCopyRecord { + seeds: ZeroCopyRecordSeeds { + owner: cached.owner, + }, + data: zc_data, + }; + let zc_spec = PdaSpec::new(zc_interface, zc_variant, ctx.program_id); + + // ATA + let ata_interface = ctx + .rpc + .get_ata_interface(&pdas.ata_owner, &pdas.mint) + .await + .expect("failed to get ATA interface"); + assert!(ata_interface.is_cold(), "ATA should be cold"); + + // Token PDA: Vault + let vault_iface = ctx + .rpc + .get_token_account_interface(&pdas.vault) + .await + .expect("failed to get vault interface"); + assert!(vault_iface.is_cold(), "Vault should be cold"); + + let vault_token_data: Token = + borsh::BorshDeserialize::deserialize(&mut &vault_iface.account.data[..]) + .expect("Failed to parse vault Token"); + let vault_variant = LightAccountVariant::Vault(TokenDataWithSeeds { + seeds: VaultSeeds { + mint: pdas.mint.to_bytes(), + }, + token_data: vault_token_data, + }); + let vault_compressed = vault_iface + .compressed() + .expect("cold vault must have compressed data"); + let vault_interface = AccountInterface { + key: vault_iface.key, + account: vault_iface.account.clone(), + cold: Some(ColdContext::Account(vault_compressed.account.clone())), + }; + let vault_spec = PdaSpec::new(vault_interface, vault_variant, ctx.program_id); + + // Mint + let mint_iface = ctx + .rpc + .get_mint_interface(&pdas.mint) + .await + .expect("failed to get mint interface"); + assert!(mint_iface.is_cold(), "Mint should be cold"); + let (compressed_mint, _) = mint_iface + .compressed() + .expect("cold mint must have compressed data"); + let mint_ai = AccountInterface { + key: pdas.mint, + account: solana_account::Account { + lamports: 0, + data: vec![], + owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Account(compressed_mint.clone())), + }; + + let specs: Vec> = vec![ + AccountSpec::Pda(record_spec), + AccountSpec::Pda(zc_spec), + AccountSpec::Ata(ata_interface), + AccountSpec::Pda(vault_spec), + AccountSpec::Mint(mint_ai), + ]; + + let decompress_ixs = + create_load_instructions(&specs, ctx.payer.pubkey(), ctx.config_pda, &ctx.rpc) + .await + .expect("create_load_instructions should succeed"); + + ctx.rpc + .create_and_send_transaction(&decompress_ixs, &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("Decompression should succeed"); + + // Verify all decompressed accounts exist on-chain + for (pda, name) in [ + (&pdas.record, "MinimalRecord"), + (&pdas.zc_record, "ZeroCopyRecord"), + (&pdas.ata, "ATA"), + (&pdas.vault, "Vault"), + (&pdas.mint, "Mint"), + ] { + shared::assert_onchain_exists(&mut ctx.rpc, pda, name).await; + } +} + +/// Compress all accounts by warping forward epochs +async fn compress_all(ctx: &mut StressTestContext, pdas: &TestPdas) { + ctx.rpc + .warp_slot_forward(SLOTS_PER_EPOCH * 100) + .await + .unwrap(); + + for (pda, name) in [ + (&pdas.record, "MinimalRecord"), + (&pdas.zc_record, "ZeroCopyRecord"), + (&pdas.ata, "ATA"), + (&pdas.vault, "Vault"), + (&pdas.mint, "Mint"), + ] { + shared::assert_onchain_closed(&mut ctx.rpc, pda, name).await; + } +} + +/// Full-struct assertions for all accounts against cached state +async fn assert_all_state( + rpc: &mut LightProgramTest, + pdas: &TestPdas, + cached: &CachedState, + iteration: usize, +) { + // MinimalRecord + let account = rpc.get_account(pdas.record).await.unwrap().unwrap(); + let actual_record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &account.data[8..]).unwrap(); + let expected_record = MinimalRecord { + compression_info: shared::expected_compression_info(&actual_record.compression_info), + ..cached.record.clone() + }; + assert_eq!( + actual_record, expected_record, + "MinimalRecord mismatch at iteration {iteration}" + ); + + // ZeroCopyRecord + let account = rpc.get_account(pdas.zc_record).await.unwrap().unwrap(); + let actual_zc: &ZeroCopyRecord = bytemuck::from_bytes(&account.data[8..]); + let expected_zc = ZeroCopyRecord { + compression_info: shared::expected_compression_info(&actual_zc.compression_info), + ..cached.zc_record + }; + assert_eq!( + *actual_zc, expected_zc, + "ZeroCopyRecord mismatch at iteration {iteration}" + ); + + // ATA + let actual_ata = parse_token(&rpc.get_account(pdas.ata).await.unwrap().unwrap().data); + let expected_ata = Token { + extensions: actual_ata.extensions.clone(), + ..cached.ata_token.clone() + }; + assert_eq!( + actual_ata, expected_ata, + "ATA mismatch at iteration {iteration}" + ); + + // Vault + let actual_vault = parse_token(&rpc.get_account(pdas.vault).await.unwrap().unwrap().data); + let expected_vault = Token { + extensions: actual_vault.extensions.clone(), + ..cached.vault_token.clone() + }; + assert_eq!( + actual_vault, expected_vault, + "Vault mismatch at iteration {iteration}" + ); + + // Mint + let actual_mint: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(pdas.mint).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!( + actual_mint.base.decimals, 9, + "Mint decimals mismatch at iteration {iteration}" + ); +} + +#[tokio::test] +async fn test_stress_20_iterations() { + let (mut ctx, pdas) = setup().await; + + // Verify initial creation + for (pda, name) in [ + (&pdas.record, "MinimalRecord"), + (&pdas.zc_record, "ZeroCopyRecord"), + (&pdas.ata, "ATA"), + (&pdas.vault, "Vault"), + (&pdas.mint, "Mint"), + ] { + shared::assert_onchain_exists(&mut ctx.rpc, pda, name).await; + } + + // Cache initial state + let owner = { + let account = ctx.rpc.get_account(pdas.record).await.unwrap().unwrap(); + let record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &account.data[8..]).unwrap(); + record.owner + }; + let mut cached = refresh_cache(&mut ctx.rpc, &pdas, owner).await; + + // First compression + compress_all(&mut ctx, &pdas).await; + + // Main loop: 20 iterations + for i in 0..20 { + println!("--- Iteration {i} ---"); + + // Decompress all + decompress_all(&mut ctx, &pdas, &cached).await; + + // Assert all cached state + assert_all_state(&mut ctx.rpc, &pdas, &cached, i).await; + + // Update cache after decompression (compression_info changes) + cached = refresh_cache(&mut ctx.rpc, &pdas, owner).await; + + // Compress all + compress_all(&mut ctx, &pdas).await; + + println!(" iteration {i} complete"); + } + + println!("All 20 iterations completed successfully."); +} diff --git a/sdk-tests/pinocchio-light-program-test/tests/test_create_all.rs b/sdk-tests/pinocchio-light-program-test/tests/test_create_all.rs new file mode 100644 index 0000000000..4b505dfe15 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/tests/test_create_all.rs @@ -0,0 +1,399 @@ +mod shared; + +use light_account_pinocchio::token::TokenDataWithSeeds; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountInterfaceExt, + AccountSpec, ColdContext, CreateAccountsProofInput, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_token_interface::state::token::{AccountState, Token, ACCOUNT_TYPE_TOKEN_ACCOUNT}; +use pinocchio_light_program_test::{ + all::accounts::CreateAllParams, discriminators, LightAccountVariant, MinimalRecord, + MinimalRecordSeeds, VaultSeeds, ZeroCopyRecord, ZeroCopyRecordSeeds, MINT_SIGNER_SEED_A, + RECORD_SEED, VAULT_AUTH_SEED, VAULT_SEED, +}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_all_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let owner = Keypair::new().pubkey(); + let authority = Keypair::new(); + + // PDA: MinimalRecord + let (record_pda, _) = + Pubkey::find_program_address(&[b"minimal_record", owner.as_ref()], &program_id); + + // PDA: ZeroCopyRecord + let (zc_record_pda, _) = + Pubkey::find_program_address(&[RECORD_SEED, owner.as_ref()], &program_id); + + // Mint signer PDA + let (mint_signer, mint_signer_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_A, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_pda, _) = light_token::instruction::find_mint_address(&mint_signer); + + // Token vault PDA (uses the mint we're creating) + let (vault_owner, _) = Pubkey::find_program_address(&[VAULT_AUTH_SEED], &program_id); + let (vault, vault_bump) = + Pubkey::find_program_address(&[VAULT_SEED, mint_pda.as_ref()], &program_id); + + // ATA (uses the mint we're creating) + let ata_owner = payer.pubkey(); + let (ata, _) = light_token::instruction::derive_token_ata(&ata_owner, &mint_pda); + + // Build proof inputs for PDA accounts and the mint + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::pda(record_pda), + CreateAccountsProofInput::pda(zc_record_pda), + CreateAccountsProofInput::mint(mint_signer), + ], + ) + .await + .unwrap(); + + let params = CreateAllParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner: owner.to_bytes(), + mint_signer_bump, + token_vault_bump: vault_bump, + }; + + // Account order per all/accounts.rs: + // [0] payer (signer, writable) + // [1] authority (signer) + // [2] compression_config + // [3] borsh_record (writable) + // [4] zero_copy_record (writable) + // [5] mint_signer + // [6] mint (writable) + // [7] token_vault (writable) + // [8] vault_owner + // [9] ata_owner + // [10] user_ata (writable) + // [11] compressible_config (LIGHT_TOKEN_CONFIG) + // [12] rent_sponsor (LIGHT_TOKEN_RENT_SPONSOR, writable) + // [13] light_token_program + // [14] cpi_authority + // [15] system_program + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(authority.pubkey(), true), + AccountMeta::new_readonly(env.config_pda, false), + AccountMeta::new(record_pda, false), + AccountMeta::new(zc_record_pda, false), + AccountMeta::new_readonly(mint_signer, false), + AccountMeta::new(mint_pda, false), + AccountMeta::new(vault, false), + AccountMeta::new_readonly(vault_owner, false), + AccountMeta::new_readonly(ata_owner, false), + AccountMeta::new(ata, false), + AccountMeta::new_readonly(LIGHT_TOKEN_CONFIG, false), + AccountMeta::new(LIGHT_TOKEN_RENT_SPONSOR, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID.into(), false), + AccountMeta::new_readonly(light_token_types::CPI_AUTHORITY_PDA.into(), false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let instruction = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: shared::build_instruction_data(&discriminators::CREATE_ALL, ¶ms), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateAll should succeed"); + + // PHASE 1: Verify all accounts on-chain after creation + use light_compressed_account::pubkey::Pubkey as LPubkey; + + let record_account = rpc + .get_account(record_pda) + .await + .unwrap() + .expect("Record PDA should exist"); + let record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &record_account.data[8..]) + .expect("Failed to deserialize MinimalRecord"); + assert_eq!(record.owner, owner.to_bytes(), "Record owner should match"); + + let zc_account = rpc + .get_account(zc_record_pda) + .await + .unwrap() + .expect("Zero-copy record should exist"); + let zc_record: &ZeroCopyRecord = bytemuck::from_bytes(&zc_account.data[8..]); + assert_eq!( + zc_record.owner, + owner.to_bytes(), + "ZC record owner should match" + ); + assert_eq!(zc_record.counter, 0, "ZC record counter should be 0"); + + let ata_account = rpc + .get_account(ata) + .await + .unwrap() + .expect("ATA should exist"); + let ata_token: Token = borsh::BorshDeserialize::deserialize(&mut &ata_account.data[..]) + .expect("Failed to deserialize ATA Token"); + assert_eq!( + ata_token.mint, + LPubkey::from(mint_pda.to_bytes()), + "ATA mint should match" + ); + assert_eq!( + ata_token.owner, + LPubkey::from(ata_owner.to_bytes()), + "ATA owner should match" + ); + + let vault_account = rpc + .get_account(vault) + .await + .unwrap() + .expect("Vault should exist"); + let vault_token: Token = borsh::BorshDeserialize::deserialize(&mut &vault_account.data[..]) + .expect("Failed to deserialize Vault Token"); + assert_eq!( + vault_token.mint, + LPubkey::from(mint_pda.to_bytes()), + "Vault mint should match" + ); + assert_eq!( + vault_token.owner, + LPubkey::from(vault_owner.to_bytes()), + "Vault owner should match" + ); + + use light_token_interface::state::Mint; + + let mint_account = rpc + .get_account(mint_pda) + .await + .unwrap() + .expect("Mint should exist"); + let mint: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_account.data[..]) + .expect("Failed to deserialize Mint"); + assert_eq!(mint.base.decimals, 9, "Mint should have 9 decimals"); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + + shared::assert_onchain_closed(&mut rpc, &record_pda, "MinimalRecord").await; + shared::assert_onchain_closed(&mut rpc, &zc_record_pda, "ZeroCopyRecord").await; + shared::assert_onchain_closed(&mut rpc, &ata, "ATA").await; + shared::assert_onchain_closed(&mut rpc, &vault, "Vault").await; + shared::assert_onchain_closed(&mut rpc, &mint_pda, "Mint").await; + + // PHASE 3: Decompress all accounts via create_load_instructions + + // PDA: MinimalRecord + let record_interface = rpc + .get_account_interface(&record_pda, &program_id) + .await + .expect("failed to get MinimalRecord interface"); + assert!(record_interface.is_cold(), "MinimalRecord should be cold"); + + let record_data: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &record_interface.account.data[8..]) + .expect("Failed to parse MinimalRecord"); + let record_variant = LightAccountVariant::MinimalRecord { + seeds: MinimalRecordSeeds { + owner: owner.to_bytes(), + }, + data: record_data, + }; + let record_spec = PdaSpec::new(record_interface, record_variant, program_id); + + // PDA: ZeroCopyRecord + let zc_interface = rpc + .get_account_interface(&zc_record_pda, &program_id) + .await + .expect("failed to get ZeroCopyRecord interface"); + assert!(zc_interface.is_cold(), "ZeroCopyRecord should be cold"); + + let zc_data: ZeroCopyRecord = + borsh::BorshDeserialize::deserialize(&mut &zc_interface.account.data[8..]) + .expect("Failed to parse ZeroCopyRecord"); + let zc_variant = LightAccountVariant::ZeroCopyRecord { + seeds: ZeroCopyRecordSeeds { + owner: owner.to_bytes(), + }, + data: zc_data, + }; + let zc_spec = PdaSpec::new(zc_interface, zc_variant, program_id); + + // ATA + let ata_interface = rpc + .get_ata_interface(&ata_owner, &mint_pda) + .await + .expect("failed to get ATA interface"); + assert!(ata_interface.is_cold(), "ATA should be cold"); + + // Token PDA: Vault + let vault_iface = rpc + .get_token_account_interface(&vault) + .await + .expect("failed to get vault interface"); + assert!(vault_iface.is_cold(), "Vault should be cold"); + + let vault_token_data: Token = + borsh::BorshDeserialize::deserialize(&mut &vault_iface.account.data[..]) + .expect("Failed to parse vault Token"); + let vault_variant = LightAccountVariant::Vault(TokenDataWithSeeds { + seeds: VaultSeeds { + mint: mint_pda.to_bytes(), + }, + token_data: vault_token_data, + }); + let vault_compressed = vault_iface + .compressed() + .expect("cold vault must have compressed data"); + let vault_interface = AccountInterface { + key: vault_iface.key, + account: vault_iface.account.clone(), + cold: Some(ColdContext::Account(vault_compressed.account.clone())), + }; + let vault_spec = PdaSpec::new(vault_interface, vault_variant, program_id); + + // Mint + let mint_iface = rpc + .get_mint_interface(&mint_pda) + .await + .expect("failed to get mint interface"); + assert!(mint_iface.is_cold(), "Mint should be cold"); + let (compressed_mint, _) = mint_iface + .compressed() + .expect("cold mint must have compressed data"); + let mint_ai = AccountInterface { + key: mint_pda, + account: solana_account::Account { + lamports: 0, + data: vec![], + owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Account(compressed_mint.clone())), + }; + + let specs: Vec> = vec![ + AccountSpec::Pda(record_spec), + AccountSpec::Pda(zc_spec), + AccountSpec::Ata(ata_interface), + AccountSpec::Pda(vault_spec), + AccountSpec::Mint(mint_ai), + ]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &record_pda, "MinimalRecord").await; + shared::assert_onchain_exists(&mut rpc, &zc_record_pda, "ZeroCopyRecord").await; + shared::assert_onchain_exists(&mut rpc, &ata, "ATA").await; + shared::assert_onchain_exists(&mut rpc, &vault, "Vault").await; + shared::assert_onchain_exists(&mut rpc, &mint_pda, "Mint").await; + + // MinimalRecord + let account = rpc.get_account(record_pda).await.unwrap().unwrap(); + let actual_record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &account.data[8..]).unwrap(); + let expected_record = MinimalRecord { + compression_info: shared::expected_compression_info(&actual_record.compression_info), + owner: owner.to_bytes(), + }; + assert_eq!( + actual_record, expected_record, + "MinimalRecord should match after decompression" + ); + + // ZeroCopyRecord + let account = rpc.get_account(zc_record_pda).await.unwrap().unwrap(); + let actual_zc: &ZeroCopyRecord = bytemuck::from_bytes(&account.data[8..]); + let expected_zc = ZeroCopyRecord { + compression_info: shared::expected_compression_info(&actual_zc.compression_info), + owner: owner.to_bytes(), + counter: 0, + }; + assert_eq!( + *actual_zc, expected_zc, + "ZeroCopyRecord should match after decompression" + ); + + // ATA + let actual_ata: Token = shared::parse_token(&rpc.get_account(ata).await.unwrap().unwrap().data); + let expected_ata = Token { + mint: LPubkey::from(mint_pda.to_bytes()), + owner: LPubkey::from(ata_owner.to_bytes()), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: actual_ata.extensions.clone(), + }; + assert_eq!( + actual_ata, expected_ata, + "ATA should match after decompression" + ); + + // Vault + let actual_vault: Token = + shared::parse_token(&rpc.get_account(vault).await.unwrap().unwrap().data); + let expected_vault = Token { + mint: LPubkey::from(mint_pda.to_bytes()), + owner: LPubkey::from(vault_owner.to_bytes()), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: actual_vault.extensions.clone(), + }; + assert_eq!( + actual_vault, expected_vault, + "Vault should match after decompression" + ); + + // Mint + let actual_mint: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(mint_pda).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!( + actual_mint.base.decimals, 9, + "Mint decimals should be preserved" + ); + assert_eq!( + actual_mint.base.mint_authority, + Some(authority.pubkey().to_bytes().into()), + "Mint authority should be preserved" + ); +} diff --git a/sdk-tests/pinocchio-light-program-test/tests/test_create_ata.rs b/sdk-tests/pinocchio-light-program-test/tests/test_create_ata.rs new file mode 100644 index 0000000000..73630b6a3f --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/tests/test_create_ata.rs @@ -0,0 +1,122 @@ +mod shared; + +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use pinocchio_light_program_test::{ + ata::accounts::CreateAtaParams, discriminators, LightAccountVariant, +}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_ata_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let (mint, _mint_seed) = shared::setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9).await; + + let ata_owner = payer.pubkey(); + let (ata, _ata_bump) = light_token::instruction::derive_token_ata(&ata_owner, &mint); + + let proof_result = get_create_accounts_proof(&rpc, &program_id, vec![]) + .await + .unwrap(); + + let params = CreateAtaParams {}; + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(mint, false), + AccountMeta::new_readonly(ata_owner, false), + AccountMeta::new(ata, false), + AccountMeta::new_readonly(LIGHT_TOKEN_CONFIG, false), + AccountMeta::new(LIGHT_TOKEN_RENT_SPONSOR, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID.into(), false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let instruction = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: shared::build_instruction_data(&discriminators::CREATE_ATA, ¶ms), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .expect("CreateAta should succeed"); + + // PHASE 1: Verify on-chain after creation + let ata_account = rpc + .get_account(ata) + .await + .unwrap() + .expect("ATA should exist on-chain"); + + use light_token_interface::state::token::{AccountState, Token, ACCOUNT_TYPE_TOKEN_ACCOUNT}; + let token: Token = borsh::BorshDeserialize::deserialize(&mut &ata_account.data[..]) + .expect("Failed to deserialize Token"); + + let expected_token = Token { + mint: mint.to_bytes().into(), + owner: ata_owner.to_bytes().into(), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: token.extensions.clone(), + }; + + assert_eq!( + token, expected_token, + "ATA should match expected after creation" + ); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &ata, "ATA").await; + + // PHASE 3: Decompress via create_load_instructions + let ata_interface = rpc + .get_ata_interface(&ata_owner, &mint) + .await + .expect("failed to get ATA interface"); + assert!(ata_interface.is_cold(), "ATA should be cold"); + + let specs: Vec> = vec![AccountSpec::Ata(ata_interface)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &ata, "ATA").await; + + let actual: Token = shared::parse_token(&rpc.get_account(ata).await.unwrap().unwrap().data); + let expected = Token { + mint: mint.to_bytes().into(), + owner: ata_owner.to_bytes().into(), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: actual.extensions.clone(), + }; + assert_eq!(actual, expected, "ATA should match after decompression"); +} diff --git a/sdk-tests/pinocchio-light-program-test/tests/test_create_mint.rs b/sdk-tests/pinocchio-light-program-test/tests/test_create_mint.rs new file mode 100644 index 0000000000..30bd074149 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/tests/test_create_mint.rs @@ -0,0 +1,148 @@ +mod shared; + +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountInterfaceExt, + AccountSpec, ColdContext, CreateAccountsProofInput, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use pinocchio_light_program_test::{ + discriminators, mint::accounts::CreateMintParams, LightAccountVariant, MINT_SIGNER_SEED_A, +}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_mint_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let authority = Keypair::new(); + + let (mint_signer_pda, mint_signer_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_A, authority.pubkey().as_ref()], + &program_id, + ); + + let (mint_pda, _) = light_token::instruction::find_mint_address(&mint_signer_pda); + + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::mint(mint_signer_pda)], + ) + .await + .unwrap(); + + let params = CreateMintParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump, + }; + + // Account order per mint/accounts.rs: + // [0] payer (signer, writable) + // [1] authority (signer) + // [2] mint_signer + // [3] mint (writable) + // [4] compressible_config (LIGHT_TOKEN_CONFIG) + // [5] rent_sponsor (LIGHT_TOKEN_RENT_SPONSOR, writable) + // [6] light_token_program + // [7] cpi_authority + // [8] system_program + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(authority.pubkey(), true), + AccountMeta::new_readonly(mint_signer_pda, false), + AccountMeta::new(mint_pda, false), + AccountMeta::new_readonly(LIGHT_TOKEN_CONFIG, false), + AccountMeta::new(LIGHT_TOKEN_RENT_SPONSOR, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID.into(), false), + AccountMeta::new_readonly(light_token_types::CPI_AUTHORITY_PDA.into(), false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let instruction = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: shared::build_instruction_data(&discriminators::CREATE_MINT, ¶ms), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateMint should succeed"); + + // PHASE 1: Verify on-chain after creation + let mint_account = rpc + .get_account(mint_pda) + .await + .unwrap() + .expect("Mint should exist on-chain"); + + use light_token_interface::state::Mint; + let mint: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_account.data[..]) + .expect("Failed to deserialize Mint"); + + assert_eq!(mint.base.decimals, 9, "Mint should have 9 decimals"); + assert_eq!( + mint.base.mint_authority, + Some(authority.pubkey().to_bytes().into()), + "Mint authority should be authority" + ); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &mint_pda, "Mint").await; + + // PHASE 3: Decompress via create_load_instructions + let mint_interface = rpc + .get_mint_interface(&mint_pda) + .await + .expect("failed to get mint interface"); + assert!(mint_interface.is_cold(), "Mint should be cold"); + + let (compressed, _mint_data) = mint_interface + .compressed() + .expect("cold mint must have compressed data"); + let mint_account_interface = AccountInterface { + key: mint_pda, + account: solana_account::Account { + lamports: 0, + data: vec![], + owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Account(compressed.clone())), + }; + + let specs: Vec> = + vec![AccountSpec::Mint(mint_account_interface)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &mint_pda, "Mint").await; + + let actual: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(mint_pda).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!(actual.base.decimals, 9, "Mint decimals should be preserved"); + assert_eq!( + actual.base.mint_authority, + Some(authority.pubkey().to_bytes().into()), + "Mint authority should be preserved" + ); +} diff --git a/sdk-tests/pinocchio-light-program-test/tests/test_create_pda.rs b/sdk-tests/pinocchio-light-program-test/tests/test_create_pda.rs new file mode 100644 index 0000000000..ac8af4db94 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/tests/test_create_pda.rs @@ -0,0 +1,127 @@ +mod shared; + +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, + CreateAccountsProofInput, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use pinocchio_light_program_test::{ + discriminators, pda::accounts::CreatePdaParams, LightAccountVariant, MinimalRecord, + MinimalRecordSeeds, +}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_single_pda_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let owner = Keypair::new().pubkey(); + + let (record_pda, _) = + Pubkey::find_program_address(&[b"minimal_record", owner.as_ref()], &program_id); + + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::pda(record_pda)], + ) + .await + .unwrap(); + + let params = CreatePdaParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner: owner.to_bytes(), + }; + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(env.config_pda, false), + AccountMeta::new(env.rent_sponsor, false), + AccountMeta::new(record_pda, false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let instruction = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: shared::build_instruction_data(&discriminators::CREATE_PDA, ¶ms), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .expect("CreatePda should succeed"); + + // PHASE 1: Verify on-chain after creation + let record_account = rpc + .get_account(record_pda) + .await + .unwrap() + .expect("Record PDA should exist on-chain"); + + let record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &record_account.data[8..]) + .expect("Failed to deserialize MinimalRecord"); + + let expected = MinimalRecord { + compression_info: shared::expected_compression_info(&record.compression_info), + owner: owner.to_bytes(), + }; + assert_eq!( + record, expected, + "MinimalRecord should match after creation" + ); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &record_pda, "MinimalRecord").await; + + // PHASE 3: Decompress via create_load_instructions + let account_interface = rpc + .get_account_interface(&record_pda, &program_id) + .await + .expect("failed to get MinimalRecord interface"); + assert!(account_interface.is_cold(), "MinimalRecord should be cold"); + + let data: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &account_interface.account.data[8..]) + .expect("Failed to parse MinimalRecord from interface"); + let variant = LightAccountVariant::MinimalRecord { + seeds: MinimalRecordSeeds { + owner: owner.to_bytes(), + }, + data, + }; + + let spec = PdaSpec::new(account_interface, variant, program_id); + let specs: Vec> = vec![AccountSpec::Pda(spec)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &record_pda, "MinimalRecord").await; + + let account = rpc.get_account(record_pda).await.unwrap().unwrap(); + let actual: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &account.data[8..]).unwrap(); + let expected = MinimalRecord { + compression_info: shared::expected_compression_info(&actual.compression_info), + owner: owner.to_bytes(), + }; + assert_eq!( + actual, expected, + "MinimalRecord should match after decompression" + ); +} diff --git a/sdk-tests/pinocchio-light-program-test/tests/test_create_token_vault.rs b/sdk-tests/pinocchio-light-program-test/tests/test_create_token_vault.rs new file mode 100644 index 0000000000..777bfa4bb4 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/tests/test_create_token_vault.rs @@ -0,0 +1,161 @@ +mod shared; + +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountInterfaceExt, + AccountSpec, ColdContext, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_token_interface::state::token::{AccountState, Token, ACCOUNT_TYPE_TOKEN_ACCOUNT}; +use pinocchio_light_program_test::{ + discriminators, token_account::accounts::CreateTokenVaultParams, LightAccountVariant, + VaultSeeds, VAULT_AUTH_SEED, VAULT_SEED, +}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_token_vault_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let (mint, _mint_seed) = shared::setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9).await; + + let (vault_authority, _auth_bump) = + Pubkey::find_program_address(&[VAULT_AUTH_SEED], &program_id); + let (vault, vault_bump) = + Pubkey::find_program_address(&[VAULT_SEED, mint.as_ref()], &program_id); + + let proof_result = get_create_accounts_proof(&rpc, &program_id, vec![]) + .await + .unwrap(); + + let params = CreateTokenVaultParams { vault_bump }; + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(mint, false), + AccountMeta::new_readonly(vault_authority, false), + AccountMeta::new(vault, false), + AccountMeta::new_readonly(LIGHT_TOKEN_CONFIG, false), + AccountMeta::new(LIGHT_TOKEN_RENT_SPONSOR, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID.into(), false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let instruction = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: shared::build_instruction_data(&discriminators::CREATE_TOKEN_VAULT, ¶ms), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .expect("CreateTokenVault should succeed"); + + // PHASE 1: Verify on-chain after creation + let vault_account = rpc + .get_account(vault) + .await + .unwrap() + .expect("Token vault should exist on-chain"); + + let token: Token = borsh::BorshDeserialize::deserialize(&mut &vault_account.data[..]) + .expect("Failed to deserialize Token"); + + let expected_token = Token { + mint: mint.to_bytes().into(), + owner: vault_authority.to_bytes().into(), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: token.extensions.clone(), + }; + + assert_eq!( + token, expected_token, + "Token vault should match expected after creation" + ); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &vault, "Vault").await; + + // PHASE 3: Decompress vault + let vault_iface = rpc + .get_token_account_interface(&vault) + .await + .expect("failed to get vault interface"); + assert!(vault_iface.is_cold(), "Vault should be cold"); + + let token_data: Token = + borsh::BorshDeserialize::deserialize(&mut &vault_iface.account.data[..]) + .expect("Failed to parse vault Token"); + let vault_variant = + LightAccountVariant::Vault(light_account_pinocchio::token::TokenDataWithSeeds { + seeds: VaultSeeds { + mint: mint.to_bytes(), + }, + token_data, + }); + let vault_compressed = vault_iface + .compressed() + .expect("cold vault must have compressed data"); + let vault_interface = AccountInterface { + key: vault_iface.key, + account: vault_iface.account.clone(), + cold: Some(ColdContext::Account(vault_compressed.account.clone())), + }; + let vault_spec = PdaSpec::new(vault_interface, vault_variant, program_id); + + let specs: Vec> = vec![AccountSpec::Pda(vault_spec)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Vault decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &vault, "Vault").await; + + let vault_account = rpc + .get_account(vault) + .await + .unwrap() + .expect("Vault should exist on-chain after decompression"); + + let actual_token: Token = borsh::BorshDeserialize::deserialize(&mut &vault_account.data[..]) + .expect("Failed to deserialize Token after decompression"); + + use light_compressed_account::pubkey::Pubkey as LPubkey; + + let expected_token = Token { + mint: LPubkey::from(mint.to_bytes()), + owner: LPubkey::from(vault_authority.to_bytes()), + amount: 0, + delegate: None, + state: AccountState::Initialized, + is_native: None, + delegated_amount: 0, + close_authority: None, + account_type: ACCOUNT_TYPE_TOKEN_ACCOUNT, + extensions: actual_token.extensions.clone(), + }; + + assert_eq!( + actual_token, expected_token, + "Token vault should match expected after decompression" + ); +} diff --git a/sdk-tests/pinocchio-light-program-test/tests/test_create_two_mints.rs b/sdk-tests/pinocchio-light-program-test/tests/test_create_two_mints.rs new file mode 100644 index 0000000000..ef04c1eed6 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/tests/test_create_two_mints.rs @@ -0,0 +1,204 @@ +mod shared; + +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterface, AccountInterfaceExt, + AccountSpec, ColdContext, CreateAccountsProofInput, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use pinocchio_light_program_test::{ + discriminators, two_mints::accounts::CreateTwoMintsParams, LightAccountVariant, + MINT_SIGNER_SEED_A, MINT_SIGNER_SEED_B, +}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_two_mints_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let authority = Keypair::new(); + + let (mint_signer_a, mint_signer_bump_a) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_A, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_a_pda, _) = light_token::instruction::find_mint_address(&mint_signer_a); + + let (mint_signer_b, mint_signer_bump_b) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED_B, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_b_pda, _) = light_token::instruction::find_mint_address(&mint_signer_b); + + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::mint(mint_signer_a), + CreateAccountsProofInput::mint(mint_signer_b), + ], + ) + .await + .unwrap(); + + let params = CreateTwoMintsParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump_a, + mint_signer_bump_b, + }; + + // Account order per two_mints/accounts.rs: + // [0] payer (signer, writable) + // [1] authority (signer) + // [2] mint_signer_a + // [3] mint_signer_b + // [4] mint_a (writable) + // [5] mint_b (writable) + // [6] compressible_config (LIGHT_TOKEN_CONFIG) + // [7] rent_sponsor (LIGHT_TOKEN_RENT_SPONSOR, writable) + // [8] light_token_program + // [9] cpi_authority + // [10] system_program + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(authority.pubkey(), true), + AccountMeta::new_readonly(mint_signer_a, false), + AccountMeta::new_readonly(mint_signer_b, false), + AccountMeta::new(mint_a_pda, false), + AccountMeta::new(mint_b_pda, false), + AccountMeta::new_readonly(LIGHT_TOKEN_CONFIG, false), + AccountMeta::new(LIGHT_TOKEN_RENT_SPONSOR, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID.into(), false), + AccountMeta::new_readonly(light_token_types::CPI_AUTHORITY_PDA.into(), false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let instruction = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: shared::build_instruction_data(&discriminators::CREATE_TWO_MINTS, ¶ms), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateTwoMints should succeed"); + + // PHASE 1: Verify on-chain after creation + use light_token_interface::state::Mint; + + let mint_a_account = rpc + .get_account(mint_a_pda) + .await + .unwrap() + .expect("Mint A should exist on-chain"); + let mint_a: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_a_account.data[..]) + .expect("Failed to deserialize Mint A"); + assert_eq!(mint_a.base.decimals, 9, "Mint A should have 9 decimals"); + assert_eq!( + mint_a.base.mint_authority, + Some(authority.pubkey().to_bytes().into()), + "Mint A authority should be authority" + ); + + let mint_b_account = rpc + .get_account(mint_b_pda) + .await + .unwrap() + .expect("Mint B should exist on-chain"); + let mint_b: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_b_account.data[..]) + .expect("Failed to deserialize Mint B"); + assert_eq!(mint_b.base.decimals, 6, "Mint B should have 6 decimals"); + assert_eq!( + mint_b.base.mint_authority, + Some(authority.pubkey().to_bytes().into()), + "Mint B authority should be authority" + ); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &mint_a_pda, "MintA").await; + shared::assert_onchain_closed(&mut rpc, &mint_b_pda, "MintB").await; + + // PHASE 3: Decompress both mints via create_load_instructions + let build_mint_account_interface = |mint_interface: light_client::interface::MintInterface| { + let (compressed, _mint_data) = mint_interface + .compressed() + .expect("cold mint must have compressed data"); + AccountInterface { + key: mint_interface.mint, + account: solana_account::Account { + lamports: 0, + data: vec![], + owner: light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + cold: Some(ColdContext::Account(compressed.clone())), + } + }; + + let mint_a_interface = rpc + .get_mint_interface(&mint_a_pda) + .await + .expect("failed to get mint A interface"); + assert!(mint_a_interface.is_cold(), "Mint A should be cold"); + let mint_a_ai = build_mint_account_interface(mint_a_interface); + + let mint_b_interface = rpc + .get_mint_interface(&mint_b_pda) + .await + .expect("failed to get mint B interface"); + assert!(mint_b_interface.is_cold(), "Mint B should be cold"); + let mint_b_ai = build_mint_account_interface(mint_b_interface); + + let specs: Vec> = + vec![AccountSpec::Mint(mint_a_ai), AccountSpec::Mint(mint_b_ai)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &mint_a_pda, "MintA").await; + shared::assert_onchain_exists(&mut rpc, &mint_b_pda, "MintB").await; + + let actual_a: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(mint_a_pda).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!( + actual_a.base.decimals, 9, + "Mint A decimals should be preserved" + ); + assert_eq!( + actual_a.base.mint_authority, + Some(authority.pubkey().to_bytes().into()), + "Mint A authority should be preserved" + ); + + let actual_b: Mint = borsh::BorshDeserialize::deserialize( + &mut &rpc.get_account(mint_b_pda).await.unwrap().unwrap().data[..], + ) + .unwrap(); + assert_eq!( + actual_b.base.decimals, 6, + "Mint B decimals should be preserved" + ); + assert_eq!( + actual_b.base.mint_authority, + Some(authority.pubkey().to_bytes().into()), + "Mint B authority should be preserved" + ); +} diff --git a/sdk-tests/pinocchio-light-program-test/tests/test_create_zero_copy_record.rs b/sdk-tests/pinocchio-light-program-test/tests/test_create_zero_copy_record.rs new file mode 100644 index 0000000000..955c1bb0c0 --- /dev/null +++ b/sdk-tests/pinocchio-light-program-test/tests/test_create_zero_copy_record.rs @@ -0,0 +1,120 @@ +mod shared; + +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, + CreateAccountsProofInput, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Rpc}; +use pinocchio_light_program_test::{ + account_loader::accounts::CreateZeroCopyRecordParams, discriminators, LightAccountVariant, + ZeroCopyRecord, ZeroCopyRecordSeeds, RECORD_SEED, +}; +use solana_instruction::{AccountMeta, Instruction}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +#[tokio::test] +async fn test_create_zero_copy_record_derive() { + let env = shared::setup_test_env().await; + let mut rpc = env.rpc; + let payer = env.payer; + let program_id = env.program_id; + + let owner = Keypair::new().pubkey(); + + let (record_pda, _) = Pubkey::find_program_address(&[RECORD_SEED, owner.as_ref()], &program_id); + + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::pda(record_pda)], + ) + .await + .unwrap(); + + let params = CreateZeroCopyRecordParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner: owner.to_bytes(), + }; + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(env.config_pda, false), + AccountMeta::new(env.rent_sponsor, false), + AccountMeta::new(record_pda, false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let instruction = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: shared::build_instruction_data(&discriminators::CREATE_ZERO_COPY_RECORD, ¶ms), + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .expect("CreateZeroCopyRecord should succeed"); + + // PHASE 1: Verify on-chain after creation + let record_account = rpc + .get_account(record_pda) + .await + .unwrap() + .expect("Record PDA should exist on-chain"); + + let discriminator_len = 8; + let data = &record_account.data[discriminator_len..]; + let record: &ZeroCopyRecord = bytemuck::from_bytes(data); + + assert_eq!(record.owner, owner.to_bytes(), "Record owner should match"); + assert_eq!(record.counter, 0, "Record counter should be 0"); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + shared::assert_onchain_closed(&mut rpc, &record_pda, "ZeroCopyRecord").await; + + // PHASE 3: Decompress via create_load_instructions + let account_interface = rpc + .get_account_interface(&record_pda, &program_id) + .await + .expect("failed to get ZeroCopyRecord interface"); + assert!(account_interface.is_cold(), "ZeroCopyRecord should be cold"); + + let zc_data: ZeroCopyRecord = + borsh::BorshDeserialize::deserialize(&mut &account_interface.account.data[8..]) + .expect("Failed to parse ZeroCopyRecord from interface"); + let variant = LightAccountVariant::ZeroCopyRecord { + seeds: ZeroCopyRecordSeeds { + owner: owner.to_bytes(), + }, + data: zc_data, + }; + + let spec = PdaSpec::new(account_interface, variant, program_id); + let specs: Vec> = vec![AccountSpec::Pda(spec)]; + + let ixs = create_load_instructions(&specs, payer.pubkey(), env.config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&ixs, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Assert state preserved after decompression + shared::assert_onchain_exists(&mut rpc, &record_pda, "ZeroCopyRecord").await; + + let account = rpc.get_account(record_pda).await.unwrap().unwrap(); + let actual: &ZeroCopyRecord = bytemuck::from_bytes(&account.data[8..]); + let expected = ZeroCopyRecord { + compression_info: shared::expected_compression_info(&actual.compression_info), + owner: owner.to_bytes(), + counter: 0, + }; + assert_eq!( + *actual, expected, + "ZeroCopyRecord should match after decompression" + ); +} diff --git a/sdk-tests/pinocchio-manual-test/Cargo.toml b/sdk-tests/pinocchio-manual-test/Cargo.toml new file mode 100644 index 0000000000..e60445c1c4 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "pinocchio-manual-test" +version = "0.1.0" +description = "Manual LightAccount implementation test with pinocchio (no Anchor)" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "pinocchio_manual_test" + +[features] +no-entrypoint = [] +default = [] +test-sbf = [] + +[dependencies] +light-account-pinocchio = { workspace = true, features = ["token", "std"] } +light-macros = { workspace = true, features = ["solana"] } +borsh = { workspace = true } +bytemuck = { workspace = true } +light-compressed-account = { workspace = true, features = ["solana"] } +light-compressible = { workspace = true, features = ["pinocchio"] } +light-hasher = { workspace = true, features = ["solana"] } +light-token-types = { workspace = true } +light-token-interface = { workspace = true } +pinocchio = { workspace = true } +pinocchio-pubkey = { workspace = true } +pinocchio-system = { workspace = true } +solana-pubkey = { workspace = true } +solana-instruction = { workspace = true } +solana-msg = { workspace = true } +solana-program-error = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +light-client = { workspace = true, features = ["v2", "anchor"] } +light-test-utils = { workspace = true } +light-token = { workspace = true, features = ["anchor"] } +light-token-client = { workspace = true } +tokio = { workspace = true } +solana-sdk = { workspace = true } +solana-keypair = { workspace = true } +solana-signer = { workspace = true } + +[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/pinocchio-manual-test/keypair.json b/sdk-tests/pinocchio-manual-test/keypair.json new file mode 100644 index 0000000000..48a400e300 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/keypair.json @@ -0,0 +1 @@ +[253,60,210,72,31,164,77,145,249,190,2,16,13,127,213,114,240,143,220,32,31,142,54,182,47,172,30,213,57,31,242,178,95,240,102,74,184,144,135,198,93,167,147,174,97,28,27,254,68,20,246,94,255,0,152,161,63,174,49,14,37,212,50,166] \ No newline at end of file diff --git a/sdk-tests/pinocchio-manual-test/src/account_loader/accounts.rs b/sdk-tests/pinocchio-manual-test/src/account_loader/accounts.rs new file mode 100644 index 0000000000..2a3db095e9 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/account_loader/accounts.rs @@ -0,0 +1,93 @@ +//! Accounts module for zero-copy account instruction (pinocchio version). + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::CreateAccountsProof; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, + sysvars::Sysvar, +}; + +use super::state::ZeroCopyRecord; + +/// Parameters for creating a zero-copy compressible PDA. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +pub struct CreateZeroCopyParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: [u8; 32], + pub value: u64, + pub name: String, +} + +/// Accounts struct for creating a zero-copy compressible PDA. +pub struct CreateZeroCopy<'a> { + pub fee_payer: &'a AccountInfo, + pub compression_config: &'a AccountInfo, + pub record: &'a AccountInfo, + pub system_program: &'a AccountInfo, +} + +impl<'a> CreateZeroCopy<'a> { + pub const FIXED_LEN: usize = 4; + + pub fn parse( + accounts: &'a [AccountInfo], + params: &CreateZeroCopyParams, + ) -> Result { + let fee_payer = &accounts[0]; + let compression_config = &accounts[1]; + let record = &accounts[2]; + let system_program = &accounts[3]; + + if !fee_payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Verify and create the PDA account via system program CPI + let space = 8 + ZeroCopyRecord::INIT_SPACE; + let name_bytes = params.name.as_bytes(); + let seeds: &[&[u8]] = &[b"zero_copy", ¶ms.owner, name_bytes]; + let (expected_pda, bump) = pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if record.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + + let rent = + pinocchio::sysvars::rent::Rent::get().map_err(|_| ProgramError::UnsupportedSysvar)?; + let lamports = rent.minimum_balance(space); + + let bump_bytes = [bump]; + let seed_array = [ + Seed::from(b"zero_copy" as &[u8]), + Seed::from(params.owner.as_ref()), + Seed::from(name_bytes), + Seed::from(bump_bytes.as_ref()), + ]; + let signer = Signer::from(&seed_array); + pinocchio_system::instructions::CreateAccount { + from: fee_payer, + to: record, + lamports, + space: space as u64, + owner: &crate::ID, + } + .invoke_signed(&[signer])?; + + // Write LIGHT_DISCRIMINATOR to first 8 bytes + { + use light_account_pinocchio::LightDiscriminator; + let mut data = record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + data[..8].copy_from_slice(&ZeroCopyRecord::LIGHT_DISCRIMINATOR); + } + + Ok(Self { + fee_payer, + compression_config, + record, + system_program, + }) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/account_loader/derived_accounts.rs b/sdk-tests/pinocchio-manual-test/src/account_loader/derived_accounts.rs new file mode 100644 index 0000000000..2732bd233e --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/account_loader/derived_accounts.rs @@ -0,0 +1,370 @@ +//! Variant structs and trait implementations for ZeroCopyRecord. +//! +//! This follows the same pattern as MinimalRecord's derived_accounts.rs, +//! adapted for the AccountLoader (zero-copy) access pattern. + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{ + light_account_checks::{self, packed_accounts::ProgramPackedAccounts}, + prepare_compressed_account_on_init, CpiAccounts, CpiAccountsConfig, CpiContextWriteAccounts, + InvokeLightSystemProgram, LightAccount, LightAccountVariantTrait, LightFinalize, LightPreInit, + LightSdkTypesError, PackedAddressTreeInfoExt, PackedLightAccountVariantTrait, +}; +use light_compressed_account::instruction_data::{ + cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, +}; +use pinocchio::account_info::AccountInfo; + +use super::{ + accounts::{CreateZeroCopy, CreateZeroCopyParams}, + derived_state::PackedZeroCopyRecord, + state::ZeroCopyRecord, +}; + +// ============================================================================ +// Compile-time Size Validation (800-byte limit for compressed accounts) +// ============================================================================ + +const _: () = { + const COMPRESSED_SIZE: usize = 8 + core::mem::size_of::(); + assert!( + COMPRESSED_SIZE <= 800, + "Compressed account 'ZeroCopyRecord' exceeds 800-byte compressible account size limit" + ); +}; + +// ============================================================================ +// Manual LightPreInit Implementation +// ============================================================================ + +impl LightPreInit for CreateZeroCopy<'_> { + fn light_pre_init( + &mut self, + remaining_accounts: &[AccountInfo], + params: &CreateZeroCopyParams, + ) -> std::result::Result { + let inner = || -> std::result::Result { + use light_account_pinocchio::{LightAccount, LightConfig}; + use pinocchio::sysvars::{clock::Clock, Sysvar}; + + // 1. Build CPI accounts (slice remaining_accounts at system_accounts_offset) + let system_accounts_offset = + params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + self.fee_payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + // 2. Get address tree pubkey from packed tree info + let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&cpi_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let output_tree_index = params.create_accounts_proof.output_state_tree_index; + let current_account_index: u8 = 0; + // Is true if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. + const WITH_CPI_CONTEXT: bool = false; + // Is first if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. + let cpi_context = if WITH_CPI_CONTEXT { + CompressedCpiContext::first() + } else { + CompressedCpiContext::default() + }; + const NUM_LIGHT_PDAS: usize = 1; + let mut new_address_params = Vec::with_capacity(NUM_LIGHT_PDAS); + let mut account_infos = Vec::with_capacity(NUM_LIGHT_PDAS); + + // 3. Load config and get current slot + let light_config = LightConfig::load_checked(self.compression_config, &crate::ID) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let current_slot = Clock::get() + .map_err(|_| LightSdkTypesError::InvalidInstructionData)? + .slot; + + // 4. Prepare compressed account using helper function + // Get the record's key from AccountInfo + let record_key = *self.record.key(); + prepare_compressed_account_on_init( + &record_key, + &address_tree_pubkey, + address_tree_info, + output_tree_index, + current_account_index, + &crate::ID, + &mut new_address_params, + &mut account_infos, + )?; + + // 5. Set compression_info on the zero-copy record + // For pinocchio, access data directly via try_borrow_mut_data() + bytemuck + { + let mut account_data = self + .record + .try_borrow_mut_data() + .map_err(|_| LightSdkTypesError::Borsh)?; + let record_bytes = &mut account_data[8..8 + core::mem::size_of::()]; + let record: &mut ZeroCopyRecord = bytemuck::from_bytes_mut(record_bytes); + record.set_decompressed(&light_config, current_slot); + } + + // 6. Build instruction data manually (no builder pattern) + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, // V2 mode + bump: crate::LIGHT_CPI_SIGNER.bump, + invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: WITH_CPI_CONTEXT, + with_transaction_hash: false, + cpi_context, + proof: params.create_accounts_proof.proof.0, + new_address_params, + account_infos, + read_only_addresses: vec![], + read_only_accounts: vec![], + }; + if !WITH_CPI_CONTEXT { + // 7. Invoke Light System Program CPI + instruction_data.invoke(cpi_accounts)?; + } else { + // For flows that combine light mints with light PDAs, write to CPI context first. + let cpi_context_accounts = CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority()?, + cpi_context: cpi_accounts.cpi_context()?, + cpi_signer: crate::LIGHT_CPI_SIGNER, + }; + instruction_data.invoke_write_to_cpi_context_first(cpi_context_accounts)?; + } + + Ok(false) // No mints, so no CPI context write + }; + inner() + } +} + +// ============================================================================ +// Manual LightFinalize Implementation (no-op for PDA-only flow) +// ============================================================================ + +impl LightFinalize for CreateZeroCopy<'_> { + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo], + _params: &CreateZeroCopyParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + // No-op for PDA-only flow - compression CPI already executed in light_pre_init + Ok(()) + } +} + +// ============================================================================ +// Seeds Structs +// Extracted from: seeds = [b"zero_copy", params.owner.as_ref()] +// ============================================================================ + +/// Seeds for ZeroCopyRecord PDA. +/// Contains the dynamic seed values (static prefix "zero_copy" is in seed_refs). +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct ZeroCopyRecordSeeds { + pub owner: [u8; 32], + pub name: String, +} + +/// Packed seeds with u8 indices instead of Pubkeys. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedZeroCopyRecordSeeds { + pub owner_idx: u8, + pub name: String, + pub bump: u8, +} + +// ============================================================================ +// Variant Structs +// ============================================================================ + +/// Full variant combining seeds + data. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct ZeroCopyRecordVariant { + pub seeds: ZeroCopyRecordSeeds, + pub data: ZeroCopyRecord, +} + +/// Packed variant for efficient serialization. +/// Contains packed seeds and data with u8 indices for Pubkey deduplication. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedZeroCopyRecordVariant { + pub seeds: PackedZeroCopyRecordSeeds, + pub data: PackedZeroCopyRecord, +} + +// ============================================================================ +// LightAccountVariant Implementation +// ============================================================================ + +impl LightAccountVariantTrait<4> for ZeroCopyRecordVariant { + const PROGRAM_ID: [u8; 32] = crate::ID; + + type Seeds = ZeroCopyRecordSeeds; + type Data = ZeroCopyRecord; + type Packed = PackedZeroCopyRecordVariant; + + fn data(&self) -> &Self::Data { + &self.data + } + + /// Get seed values as owned byte vectors for PDA derivation. + /// Generated from: seeds = [b"zero_copy", params.owner.as_ref(), params.name.as_bytes()] + fn seed_vec(&self) -> Vec> { + vec![ + b"zero_copy".to_vec(), + self.seeds.owner.to_vec(), + self.seeds.name.as_bytes().to_vec(), + ] + } + + /// Get seed references with bump for CPI signing. + /// Generated from: seeds = [b"zero_copy", params.owner.as_ref(), params.name.as_bytes()] + fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; 4] { + [ + b"zero_copy", + self.seeds.owner.as_ref(), + self.seeds.name.as_bytes(), + bump_storage, + ] + } +} + +// ============================================================================ +// PackedLightAccountVariant Implementation +// ============================================================================ + +impl PackedLightAccountVariantTrait<4> for PackedZeroCopyRecordVariant { + type Unpacked = ZeroCopyRecordVariant; + + const ACCOUNT_TYPE: light_account_pinocchio::AccountType = + ::ACCOUNT_TYPE; + + fn bump(&self) -> u8 { + self.seeds.bump + } + + fn unpack( + &self, + accounts: &[AI], + ) -> std::result::Result { + let owner = accounts + .get(self.seeds.owner_idx as usize) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + + // Build ProgramPackedAccounts for LightAccount::unpack + let packed_accounts = ProgramPackedAccounts { accounts }; + let data = ZeroCopyRecord::unpack(&self.data, &packed_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + + Ok(ZeroCopyRecordVariant { + seeds: ZeroCopyRecordSeeds { + owner: owner.key(), + name: self.seeds.name.clone(), + }, + data, + }) + } + + fn seed_refs_with_bump<'a, AI: light_account_checks::AccountInfoTrait>( + &'a self, + _accounts: &'a [AI], + _bump_storage: &'a [u8; 1], + ) -> std::result::Result<[&'a [u8]; 4], LightSdkTypesError> { + Err(LightSdkTypesError::InvalidSeeds) + } + + fn into_in_token_data( + &self, + _tree_info: &light_account_pinocchio::PackedStateTreeInfo, + _output_queue_index: u8, + ) -> std::result::Result< + light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext, + LightSdkTypesError, + > { + Err(LightSdkTypesError::InvalidInstructionData) + } + + fn into_in_tlv( + &self, + ) -> std::result::Result< + Option>, + LightSdkTypesError, + > { + Ok(None) + } +} + +// ============================================================================ +// IntoVariant Implementation for Seeds (client-side API) +// ============================================================================ + +/// Implement IntoVariant to allow building variant from seeds + compressed data. +/// This enables the high-level `create_load_instructions` API. +#[cfg(not(target_os = "solana"))] +impl light_account_pinocchio::IntoVariant for ZeroCopyRecordSeeds { + fn into_variant( + self, + data: &[u8], + ) -> std::result::Result { + // For ZeroCopy (Pod) accounts, data is the full Pod bytes including compression_info. + // We deserialize using BorshDeserialize (which ZeroCopyRecord implements). + let record: ZeroCopyRecord = + BorshDeserialize::deserialize(&mut &data[..]).map_err(|_| LightSdkTypesError::Borsh)?; + + // Verify the owner in data matches the seed + if record.owner != self.owner { + return Err(LightSdkTypesError::InvalidSeeds); + } + + Ok(ZeroCopyRecordVariant { + seeds: self, + data: record, + }) + } +} + +// ============================================================================ +// Pack Implementation for ZeroCopyRecordVariant (client-side API) +// ============================================================================ + +/// Implement Pack trait to allow ZeroCopyRecordVariant to be used with `create_load_instructions`. +/// Transforms the variant into PackedLightAccountVariant for efficient serialization. +#[cfg(not(target_os = "solana"))] +impl light_account_pinocchio::Pack for ZeroCopyRecordVariant { + type Packed = crate::derived_variants::PackedLightAccountVariant; + + fn pack( + &self, + accounts: &mut light_account_pinocchio::PackedAccounts, + ) -> std::result::Result { + use light_account_pinocchio::LightAccountVariantTrait; + let (_, bump) = self.derive_pda::(); + let packed_data = self + .data + .pack(accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + Ok( + crate::derived_variants::PackedLightAccountVariant::ZeroCopyRecord { + seeds: PackedZeroCopyRecordSeeds { + owner_idx: accounts + .insert_or_get(solana_pubkey::Pubkey::from(self.seeds.owner)), + name: self.seeds.name.clone(), + bump, + }, + data: packed_data, + }, + ) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/account_loader/derived_state.rs b/sdk-tests/pinocchio-manual-test/src/account_loader/derived_state.rs new file mode 100644 index 0000000000..7e2a996133 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/account_loader/derived_state.rs @@ -0,0 +1,104 @@ +//! LightAccount implementation for ZeroCopyRecord. +//! +//! This follows the same pattern as MinimalRecord's derived_state.rs, +//! but for a Pod/zero-copy account type. + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{ + light_account_checks::{packed_accounts::ProgramPackedAccounts, AccountInfoTrait}, + AccountType, CompressionInfo, HasCompressionInfo, LightAccount, LightConfig, + LightSdkTypesError, +}; + +use super::state::ZeroCopyRecord; + +// ============================================================================ +// PackedZeroCopyRecord (compression_info excluded per implementation_details.md) +// ============================================================================ + +/// Packed version of ZeroCopyRecord for efficient transmission. +/// compression_info is excluded - it's cut off during pack. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedZeroCopyRecord { + /// Index into remaining_accounts instead of full Pubkey + pub owner: u8, + /// Value field (transmitted as-is) + pub value: u64, +} + +// ============================================================================ +// LightAccount Implementation for ZeroCopyRecord +// ============================================================================ + +impl LightAccount for ZeroCopyRecord { + const ACCOUNT_TYPE: AccountType = AccountType::PdaZeroCopy; + + type Packed = PackedZeroCopyRecord; + + // CompressionInfo (24) + owner (32) + value (8) = 64 bytes + const INIT_SPACE: usize = core::mem::size_of::(); + + fn compression_info(&self) -> &CompressionInfo { + &self.compression_info + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + &mut self.compression_info + } + + fn set_decompressed(&mut self, config: &LightConfig, current_slot: u64) { + self.compression_info = CompressionInfo::new_from_config(config, current_slot); + } + + #[cfg(not(target_os = "solana"))] + fn pack( + &self, + accounts: &mut light_account_pinocchio::interface::instruction::PackedAccounts, + ) -> std::result::Result { + // compression_info excluded from packed struct (same as Borsh accounts) + Ok(PackedZeroCopyRecord { + owner: accounts.insert_or_get(AM::pubkey_from_bytes(self.owner)), + value: self.value, + }) + } + + fn unpack( + packed: &Self::Packed, + accounts: &ProgramPackedAccounts, + ) -> std::result::Result { + // Use get_u8 with a descriptive name for better error messages + let owner_account = accounts + .get_u8(packed.owner, "ZeroCopyRecord: owner") + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + + // Set compression_info to compressed() for hash verification at decompress + // (Same pattern as Borsh accounts - canonical compressed state for hashing) + // Note: key() returns [u8; 32] directly, no conversion needed + Ok(ZeroCopyRecord { + compression_info: CompressionInfo::compressed(), + owner: owner_account.key(), + value: packed.value, + }) + } +} + +impl HasCompressionInfo for ZeroCopyRecord { + fn compression_info(&self) -> std::result::Result<&CompressionInfo, LightSdkTypesError> { + Ok(&self.compression_info) + } + + fn compression_info_mut( + &mut self, + ) -> std::result::Result<&mut CompressionInfo, LightSdkTypesError> { + Ok(&mut self.compression_info) + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + panic!("compression_info_mut_opt not supported for LightAccount types (use compression_info_mut instead)") + } + + fn set_compression_info_none(&mut self) -> std::result::Result<(), LightSdkTypesError> { + self.compression_info = CompressionInfo::compressed(); + Ok(()) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/account_loader/mod.rs b/sdk-tests/pinocchio-manual-test/src/account_loader/mod.rs new file mode 100644 index 0000000000..cb8b72ba8f --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/account_loader/mod.rs @@ -0,0 +1,24 @@ +//! Zero-copy AccountLoader support for compressible PDAs. +//! +//! This module demonstrates using AccountLoader<'info, T> instead of Account<'info, T> +//! for compressible accounts. Zero-copy accounts use bytemuck Pod/Zeroable traits +//! for direct memory access without deserialization. +//! +//! Key differences from Borsh accounts: +//! - State struct: `#[repr(C)]` + `Pod + Zeroable` instead of `#[account]` +//! - Data access: `ctx.accounts.record.load_mut()?.field` instead of `ctx.accounts.record.field` +//! - On-chain layout: Fixed-size Pod layout vs Borsh serialized +//! - Hashing: Still uses `try_to_vec()` (AnchorSerialize) for consistency + +pub mod accounts; +pub mod derived_accounts; +pub mod derived_state; +pub mod state; + +pub use accounts::*; +pub use derived_accounts::{ + PackedZeroCopyRecordSeeds, PackedZeroCopyRecordVariant, ZeroCopyRecordSeeds, + ZeroCopyRecordVariant, +}; +pub use derived_state::PackedZeroCopyRecord; +pub use state::ZeroCopyRecord; diff --git a/sdk-tests/pinocchio-manual-test/src/account_loader/state.rs b/sdk-tests/pinocchio-manual-test/src/account_loader/state.rs new file mode 100644 index 0000000000..d2df3e13e0 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/account_loader/state.rs @@ -0,0 +1,42 @@ +//! Zero-copy account state for AccountLoader demonstration. + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{CompressionInfo, LightDiscriminator, LightHasherSha}; + +/// Zero-copy account for demonstrating AccountLoader integration. +/// +/// Requirements: +/// - `#[repr(C)]` for predictable field layout +/// - `Pod + Zeroable` (bytemuck) for on-chain zero-copy access +/// - `BorshSerialize + BorshDeserialize` for hashing (same as Borsh accounts) +/// - `LightDiscriminator` for dispatch +/// - compression_info field for rent tracking +/// - All fields must be Pod-compatible (no Pubkey, use [u8; 32]) +#[derive( + Default, + Debug, + Copy, + Clone, + BorshSerialize, + BorshDeserialize, + LightDiscriminator, + LightHasherSha, + bytemuck::Pod, + bytemuck::Zeroable, +)] +#[repr(C)] +pub struct ZeroCopyRecord { + /// Compression info for rent tracking (must be first for consistent packing). + /// SDK CompressionInfo is 24 bytes, Pod-compatible. + pub compression_info: CompressionInfo, + /// Owner of the record (use byte array instead of Pubkey for Pod compatibility). + pub owner: [u8; 32], + /// A value field for demonstration. + pub value: u64, +} + +impl ZeroCopyRecord { + /// Space required for this account (excluding discriminator). + /// compression_info (24) + owner (32) + value (8) = 64 bytes + pub const INIT_SPACE: usize = core::mem::size_of::(); +} diff --git a/sdk-tests/pinocchio-manual-test/src/all/accounts.rs b/sdk-tests/pinocchio-manual-test/src/all/accounts.rs new file mode 100644 index 0000000000..3018df4509 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/all/accounts.rs @@ -0,0 +1,219 @@ +//! Accounts module for create_all instruction (pinocchio version). + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::CreateAccountsProof; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, + sysvars::Sysvar, +}; + +use crate::{account_loader::ZeroCopyRecord, pda::MinimalRecord}; + +/// Seed constants for ALL module (DIFFERENT from pda/account_loader modules) +pub const ALL_BORSH_SEED: &[u8] = b"all_borsh"; +pub const ALL_ZERO_COPY_SEED: &[u8] = b"all_zero_copy"; +pub const ALL_MINT_SIGNER_SEED: &[u8] = b"all_mint_signer"; +pub const ALL_TOKEN_VAULT_SEED: &[u8] = b"all_vault"; + +/// Parameters for creating all account types in a single instruction. +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateAllParams { + /// Proof for creating PDAs and mint addresses (3 addresses: 2 PDAs + 1 Mint). + pub create_accounts_proof: CreateAccountsProof, + /// Bump for the mint signer PDA. + pub mint_signer_bump: u8, + /// Bump for the token vault PDA. + pub token_vault_bump: u8, + /// Owner pubkey (used as seed for both PDAs). + pub owner: [u8; 32], + /// Value for the zero-copy record. + pub value: u64, +} + +/// Accounts struct for creating all account types in a single instruction. +/// +/// CPI context indices: +/// - PDA 0: Borsh PDA (MinimalRecord) - index 0 +/// - PDA 1: ZeroCopy PDA (ZeroCopyRecord) - index 1 +/// - Mint 0: Compressed mint - index 2 (offset by NUM_LIGHT_PDAS=2) +pub struct CreateAllAccounts<'a> { + pub payer: &'a AccountInfo, + pub authority: &'a AccountInfo, + pub compression_config: &'a AccountInfo, + pub borsh_record: &'a AccountInfo, + pub zero_copy_record: &'a AccountInfo, + pub mint_signer: &'a AccountInfo, + pub mint: &'a AccountInfo, + pub token_vault: &'a AccountInfo, + pub vault_owner: &'a AccountInfo, + pub ata_owner: &'a AccountInfo, + pub user_ata: &'a AccountInfo, + pub compressible_config: &'a AccountInfo, + pub rent_sponsor: &'a AccountInfo, + pub light_token_program: &'a AccountInfo, + pub cpi_authority: &'a AccountInfo, + pub system_program: &'a AccountInfo, + /// Slice view for mint_signer accounts (for invoke_create_mints) + pub mint_signers_slice: &'a [AccountInfo], + /// Slice view for mint accounts (for invoke_create_mints) + pub mints_slice: &'a [AccountInfo], +} + +impl<'a> CreateAllAccounts<'a> { + pub const FIXED_LEN: usize = 16; + + pub fn parse( + accounts: &'a [AccountInfo], + params: &CreateAllParams, + ) -> Result { + let payer = &accounts[0]; + let authority = &accounts[1]; + let compression_config = &accounts[2]; + let borsh_record = &accounts[3]; + let zero_copy_record = &accounts[4]; + let mint_signer = &accounts[5]; + let mint = &accounts[6]; + let token_vault = &accounts[7]; + let vault_owner = &accounts[8]; + let ata_owner = &accounts[9]; + let user_ata = &accounts[10]; + let compressible_config = &accounts[11]; + let rent_sponsor = &accounts[12]; + let light_token_program = &accounts[13]; + let cpi_authority = &accounts[14]; + let system_program = &accounts[15]; + + // Validate signers + if !payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + if !authority.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // ==================== Create Borsh PDA ==================== + { + let space = 8 + MinimalRecord::INIT_SPACE; + let seeds: &[&[u8]] = &[ALL_BORSH_SEED, ¶ms.owner]; + let (expected_pda, bump) = pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if borsh_record.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + + let rent = pinocchio::sysvars::rent::Rent::get() + .map_err(|_| ProgramError::UnsupportedSysvar)?; + let lamports = rent.minimum_balance(space); + + let bump_bytes = [bump]; + let seed_array = [ + Seed::from(ALL_BORSH_SEED), + Seed::from(params.owner.as_ref()), + Seed::from(bump_bytes.as_ref()), + ]; + let signer = Signer::from(&seed_array); + pinocchio_system::instructions::CreateAccount { + from: payer, + to: borsh_record, + lamports, + space: space as u64, + owner: &crate::ID, + } + .invoke_signed(&[signer])?; + + // Write LIGHT_DISCRIMINATOR + use light_account_pinocchio::LightDiscriminator; + let mut data = borsh_record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + data[..8].copy_from_slice(&MinimalRecord::LIGHT_DISCRIMINATOR); + } + + // ==================== Create Zero-Copy PDA ==================== + { + let space = 8 + ZeroCopyRecord::INIT_SPACE; + let seeds: &[&[u8]] = &[ALL_ZERO_COPY_SEED, ¶ms.owner]; + let (expected_pda, bump) = pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if zero_copy_record.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + + let rent = pinocchio::sysvars::rent::Rent::get() + .map_err(|_| ProgramError::UnsupportedSysvar)?; + let lamports = rent.minimum_balance(space); + + let bump_bytes = [bump]; + let seed_array = [ + Seed::from(ALL_ZERO_COPY_SEED), + Seed::from(params.owner.as_ref()), + Seed::from(bump_bytes.as_ref()), + ]; + let signer = Signer::from(&seed_array); + pinocchio_system::instructions::CreateAccount { + from: payer, + to: zero_copy_record, + lamports, + space: space as u64, + owner: &crate::ID, + } + .invoke_signed(&[signer])?; + + // Write LIGHT_DISCRIMINATOR + use light_account_pinocchio::LightDiscriminator; + let mut data = zero_copy_record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + data[..8].copy_from_slice(&ZeroCopyRecord::LIGHT_DISCRIMINATOR); + } + + // ==================== Validate mint_signer PDA ==================== + { + let authority_key = authority.key(); + let seeds: &[&[u8]] = &[ALL_MINT_SIGNER_SEED, authority_key]; + let (expected_pda, expected_bump) = + pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if mint_signer.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + if expected_bump != params.mint_signer_bump { + return Err(ProgramError::InvalidSeeds); + } + } + + // ==================== Validate token_vault PDA ==================== + { + let mint_key = mint.key(); + let seeds: &[&[u8]] = &[ALL_TOKEN_VAULT_SEED, mint_key]; + let (expected_pda, expected_bump) = + pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if token_vault.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + if expected_bump != params.token_vault_bump { + return Err(ProgramError::InvalidSeeds); + } + } + + Ok(Self { + payer, + authority, + compression_config, + borsh_record, + zero_copy_record, + mint_signer, + mint, + token_vault, + vault_owner, + ata_owner, + user_ata, + compressible_config, + rent_sponsor, + light_token_program, + cpi_authority, + system_program, + mint_signers_slice: &accounts[5..6], + mints_slice: &accounts[6..7], + }) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/all/derived.rs b/sdk-tests/pinocchio-manual-test/src/all/derived.rs new file mode 100644 index 0000000000..84a11085cc --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/all/derived.rs @@ -0,0 +1,267 @@ +//! Derived code for create_all instruction. +//! +//! This implements LightPreInit/LightFinalize for creating all account types: +//! - 2 PDAs (Borsh + ZeroCopy) via `invoke_write_to_cpi_context_first()` +//! - 1 Mint via `CreateMints` with cpi_context_offset +//! - 1 Token Vault via `CreateTokenAccountCpi` +//! - 1 ATA via `CreateTokenAtaCpi` + +use light_account_pinocchio::{ + derive_associated_token_account, prepare_compressed_account_on_init, CpiAccounts, + CpiAccountsConfig, CpiContextWriteAccounts, CreateMints, CreateMintsStaticAccounts, + CreateTokenAccountCpi, CreateTokenAtaCpi, InvokeLightSystemProgram, LightAccount, + LightFinalize, LightPreInit, LightSdkTypesError, PackedAddressTreeInfoExt, SingleMintParams, +}; +use light_compressed_account::instruction_data::{ + cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, +}; +use pinocchio::account_info::AccountInfo; + +use super::accounts::{ + CreateAllAccounts, CreateAllParams, ALL_MINT_SIGNER_SEED, ALL_TOKEN_VAULT_SEED, +}; + +// ============================================================================ +// LightPreInit Implementation - Creates all accounts at START of instruction +// ============================================================================ + +impl LightPreInit for CreateAllAccounts<'_> { + fn light_pre_init( + &mut self, + remaining_accounts: &[AccountInfo], + params: &CreateAllParams, + ) -> std::result::Result { + let inner = || -> std::result::Result { + use light_account_pinocchio::LightConfig; + use pinocchio::sysvars::{clock::Clock, Sysvar}; + + // Constants for this instruction + const NUM_LIGHT_PDAS: usize = 2; + const NUM_LIGHT_MINTS: usize = 1; + const WITH_CPI_CONTEXT: bool = NUM_LIGHT_PDAS > 0 && NUM_LIGHT_MINTS > 0; // true + + // ==================================================================== + // 1. Build CPI accounts with cpi_context config + // ==================================================================== + let system_accounts_offset = + params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + self.payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + // ==================================================================== + // 2. Get address tree info + // ==================================================================== + let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&cpi_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let output_tree_index = params.create_accounts_proof.output_state_tree_index; + + // ==================================================================== + // 3. Load config, get current slot + // ==================================================================== + let light_config = LightConfig::load_checked(self.compression_config, &crate::ID) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let current_slot = Clock::get() + .map_err(|_| LightSdkTypesError::InvalidInstructionData)? + .slot; + + // ==================================================================== + // 4. Create PDAs via invoke_write_to_cpi_context_first() + // ==================================================================== + { + // CPI context for PDAs - set to first() since we have mints coming after + let cpi_context = CompressedCpiContext::first(); + let mut new_address_params = Vec::with_capacity(NUM_LIGHT_PDAS); + let mut account_infos = Vec::with_capacity(NUM_LIGHT_PDAS); + + // 4a. Prepare Borsh PDA (index 0) + let borsh_record_key = *self.borsh_record.key(); + prepare_compressed_account_on_init( + &borsh_record_key, + &address_tree_pubkey, + address_tree_info, + output_tree_index, + 0, // assigned_account_index = 0 + &crate::ID, + &mut new_address_params, + &mut account_infos, + )?; + // Set compression_info on the Borsh record via mut_from_account_data + { + let mut account_data = self + .borsh_record + .try_borrow_mut_data() + .map_err(|_| LightSdkTypesError::Borsh)?; + let record = + crate::pda::MinimalRecord::mut_from_account_data(&mut account_data); + record.set_decompressed(&light_config, current_slot); + } + + // 4b. Prepare ZeroCopy PDA (index 1) + let zero_copy_record_key = *self.zero_copy_record.key(); + prepare_compressed_account_on_init( + &zero_copy_record_key, + &address_tree_pubkey, + address_tree_info, + output_tree_index, + 1, // assigned_account_index = 1 + &crate::ID, + &mut new_address_params, + &mut account_infos, + )?; + { + let mut account_data = self + .zero_copy_record + .try_borrow_mut_data() + .map_err(|_| LightSdkTypesError::Borsh)?; + let record_bytes = &mut account_data + [8..8 + core::mem::size_of::()]; + let record: &mut crate::account_loader::ZeroCopyRecord = + bytemuck::from_bytes_mut(record_bytes); + record.set_decompressed(&light_config, current_slot); + } + + // 4c. Build instruction data and write to CPI context (doesn't execute yet) + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, // V2 mode + bump: crate::LIGHT_CPI_SIGNER.bump, + invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: WITH_CPI_CONTEXT, + with_transaction_hash: false, + cpi_context, + proof: params.create_accounts_proof.proof.0, + new_address_params, + account_infos, + read_only_addresses: vec![], + read_only_accounts: vec![], + }; + + // Write to CPI context first (combined execution happens with mints) + let cpi_context_accounts = CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority()?, + cpi_context: cpi_accounts.cpi_context()?, + cpi_signer: crate::LIGHT_CPI_SIGNER, + }; + instruction_data.invoke_write_to_cpi_context_first(cpi_context_accounts)?; + } + + // ==================================================================== + // 5. Create Mint via CreateMints with cpi_context_offset + // ==================================================================== + { + let authority_key = *self.authority.key(); + let mint_signer_key = *self.mint_signer.key(); + + let mint_signer_seeds: &[&[u8]] = &[ + ALL_MINT_SIGNER_SEED, + authority_key.as_ref(), + &[params.mint_signer_bump], + ]; + + let sdk_mints: [SingleMintParams<'_>; NUM_LIGHT_MINTS] = [SingleMintParams { + decimals: 6, + mint_authority: authority_key, + mint_bump: None, + freeze_authority: None, + mint_seed_pubkey: mint_signer_key, + authority_seeds: None, + mint_signer_seeds: Some(mint_signer_seeds), + token_metadata: None, + }]; + + CreateMints { + mints: &sdk_mints, + proof_data: ¶ms.create_accounts_proof, + mint_seed_accounts: self.mint_signers_slice, + mint_accounts: self.mints_slice, + static_accounts: CreateMintsStaticAccounts { + fee_payer: self.payer, + compressible_config: self.compressible_config, + rent_sponsor: self.rent_sponsor, + cpi_authority: self.cpi_authority, + }, + cpi_context_offset: NUM_LIGHT_PDAS as u8, + } + .invoke(&cpi_accounts)?; + } + + // ==================================================================== + // 6. Create Token Vault via CreateTokenAccountCpi + // ==================================================================== + { + let mint_key = *self.mint.key(); + let vault_seeds: &[&[u8]] = &[ + ALL_TOKEN_VAULT_SEED, + mint_key.as_ref(), + &[params.token_vault_bump], + ]; + + CreateTokenAccountCpi { + payer: self.payer, + account: self.token_vault, + mint: self.mint, + owner: *self.vault_owner.key(), + } + .rent_free( + self.compressible_config, + self.rent_sponsor, + self.system_program, + &crate::ID, + ) + .invoke_signed(vault_seeds)?; + } + + // ==================================================================== + // 7. Create ATA via CreateTokenAtaCpi + // ==================================================================== + { + let (_, ata_bump) = + derive_associated_token_account(self.ata_owner.key(), self.mint.key()); + + CreateTokenAtaCpi { + payer: self.payer, + owner: self.ata_owner, + mint: self.mint, + ata: self.user_ata, + bump: ata_bump, + } + .rent_free( + self.compressible_config, + self.rent_sponsor, + self.system_program, + ) + .invoke()?; + } + + Ok(WITH_CPI_CONTEXT) + }; + inner() + } +} + +// ============================================================================ +// LightFinalize Implementation - No-op for this flow +// ============================================================================ + +impl LightFinalize for CreateAllAccounts<'_> { + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo], + _params: &CreateAllParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + // All accounts were created in light_pre_init + Ok(()) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/all/derived_accounts.rs b/sdk-tests/pinocchio-manual-test/src/all/derived_accounts.rs new file mode 100644 index 0000000000..6d55d28e86 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/all/derived_accounts.rs @@ -0,0 +1,388 @@ +//! Derived account types for the all module. +//! Uses different seeds than pda/account_loader modules but reuses the data types. + +use borsh::{BorshDeserialize, BorshSerialize}; +#[cfg(not(target_os = "solana"))] +use light_account_pinocchio::AccountInfo; +use light_account_pinocchio::{ + light_account_checks::{self, packed_accounts::ProgramPackedAccounts}, + LightAccount, LightAccountVariantTrait, LightSdkTypesError, PackedLightAccountVariantTrait, +}; + +use super::accounts::{ALL_BORSH_SEED, ALL_ZERO_COPY_SEED}; +use crate::{ + account_loader::{PackedZeroCopyRecord, ZeroCopyRecord}, + pda::{MinimalRecord, PackedMinimalRecord}, +}; + +// ============================================================================ +// AllBorsh Seeds (different seed prefix from MinimalRecordSeeds) +// ============================================================================ + +/// Seeds for AllBorsh PDA. +/// Contains the dynamic seed values (static prefix "all_borsh" is in seed_refs). +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct AllBorshSeeds { + pub owner: [u8; 32], +} + +/// Packed seeds with u8 indices instead of Pubkeys. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedAllBorshSeeds { + pub owner_idx: u8, + pub bump: u8, +} + +// ============================================================================ +// AllBorsh Variant (combines AllBorshSeeds + MinimalRecord data) +// ============================================================================ + +/// Full variant combining AllBorsh seeds + MinimalRecord data. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct AllBorshVariant { + pub seeds: AllBorshSeeds, + pub data: MinimalRecord, +} + +/// Packed variant for efficient serialization. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedAllBorshVariant { + pub seeds: PackedAllBorshSeeds, + pub data: PackedMinimalRecord, +} + +// ============================================================================ +// LightAccountVariant Implementation for AllBorshVariant +// ============================================================================ + +impl LightAccountVariantTrait<3> for AllBorshVariant { + const PROGRAM_ID: [u8; 32] = crate::ID; + + type Seeds = AllBorshSeeds; + type Data = MinimalRecord; + type Packed = PackedAllBorshVariant; + + fn data(&self) -> &Self::Data { + &self.data + } + + /// Get seed values as owned byte vectors for PDA derivation. + /// Generated from: seeds = [b"all_borsh", params.owner.as_ref()] + fn seed_vec(&self) -> Vec> { + vec![ALL_BORSH_SEED.to_vec(), self.seeds.owner.to_vec()] + } + + /// Get seed references with bump for CPI signing. + /// Generated from: seeds = [b"all_borsh", params.owner.as_ref()] + fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; 3] { + [ALL_BORSH_SEED, self.seeds.owner.as_ref(), bump_storage] + } +} + +// ============================================================================ +// PackedLightAccountVariant Implementation for PackedAllBorshVariant +// ============================================================================ + +impl PackedLightAccountVariantTrait<3> for PackedAllBorshVariant { + type Unpacked = AllBorshVariant; + + const ACCOUNT_TYPE: light_account_pinocchio::AccountType = + ::ACCOUNT_TYPE; + + fn bump(&self) -> u8 { + self.seeds.bump + } + + fn unpack( + &self, + accounts: &[AI], + ) -> std::result::Result { + let owner = accounts + .get(self.seeds.owner_idx as usize) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + + // Build ProgramPackedAccounts for LightAccount::unpack + let packed_accounts = ProgramPackedAccounts { accounts }; + let data = MinimalRecord::unpack(&self.data, &packed_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + + Ok(AllBorshVariant { + seeds: AllBorshSeeds { owner: owner.key() }, + data, + }) + } + + fn seed_refs_with_bump<'a, AI: light_account_checks::AccountInfoTrait>( + &'a self, + _accounts: &'a [AI], + _bump_storage: &'a [u8; 1], + ) -> std::result::Result<[&'a [u8]; 3], LightSdkTypesError> { + Err(LightSdkTypesError::InvalidSeeds) + } + + fn into_in_token_data( + &self, + _tree_info: &light_account_pinocchio::PackedStateTreeInfo, + _output_queue_index: u8, + ) -> std::result::Result< + light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext, + LightSdkTypesError, + > { + Err(LightSdkTypesError::InvalidInstructionData) + } + + fn into_in_tlv( + &self, + ) -> std::result::Result< + Option>, + LightSdkTypesError, + > { + Ok(None) + } +} + +// ============================================================================ +// AllZeroCopy Seeds (different seed prefix from ZeroCopyRecordSeeds) +// ============================================================================ + +/// Seeds for AllZeroCopy PDA. +/// Contains the dynamic seed values (static prefix "all_zero_copy" is in seed_refs). +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct AllZeroCopySeeds { + pub owner: [u8; 32], +} + +/// Packed seeds with u8 indices instead of Pubkeys. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedAllZeroCopySeeds { + pub owner_idx: u8, + pub bump: u8, +} + +// ============================================================================ +// AllZeroCopy Variant (combines AllZeroCopySeeds + ZeroCopyRecord data) +// ============================================================================ + +/// Full variant combining AllZeroCopy seeds + ZeroCopyRecord data. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct AllZeroCopyVariant { + pub seeds: AllZeroCopySeeds, + pub data: ZeroCopyRecord, +} + +/// Packed variant for efficient serialization. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedAllZeroCopyVariant { + pub seeds: PackedAllZeroCopySeeds, + pub data: PackedZeroCopyRecord, +} + +// ============================================================================ +// LightAccountVariant Implementation for AllZeroCopyVariant +// ============================================================================ + +impl LightAccountVariantTrait<3> for AllZeroCopyVariant { + const PROGRAM_ID: [u8; 32] = crate::ID; + + type Seeds = AllZeroCopySeeds; + type Data = ZeroCopyRecord; + type Packed = PackedAllZeroCopyVariant; + + fn data(&self) -> &Self::Data { + &self.data + } + + /// Get seed values as owned byte vectors for PDA derivation. + /// Generated from: seeds = [b"all_zero_copy", params.owner.as_ref()] + fn seed_vec(&self) -> Vec> { + vec![ALL_ZERO_COPY_SEED.to_vec(), self.seeds.owner.to_vec()] + } + + /// Get seed references with bump for CPI signing. + /// Generated from: seeds = [b"all_zero_copy", params.owner.as_ref()] + fn seed_refs_with_bump<'a>(&'a self, bump_storage: &'a [u8; 1]) -> [&'a [u8]; 3] { + [ALL_ZERO_COPY_SEED, self.seeds.owner.as_ref(), bump_storage] + } +} + +// ============================================================================ +// PackedLightAccountVariant Implementation for PackedAllZeroCopyVariant +// ============================================================================ + +impl PackedLightAccountVariantTrait<3> for PackedAllZeroCopyVariant { + type Unpacked = AllZeroCopyVariant; + + const ACCOUNT_TYPE: light_account_pinocchio::AccountType = + ::ACCOUNT_TYPE; + + fn bump(&self) -> u8 { + self.seeds.bump + } + + fn unpack( + &self, + accounts: &[AI], + ) -> std::result::Result { + let owner = accounts + .get(self.seeds.owner_idx as usize) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + + // Build ProgramPackedAccounts for LightAccount::unpack + let packed_accounts = ProgramPackedAccounts { accounts }; + let data = ZeroCopyRecord::unpack(&self.data, &packed_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + + Ok(AllZeroCopyVariant { + seeds: AllZeroCopySeeds { owner: owner.key() }, + data, + }) + } + + fn seed_refs_with_bump<'a, AI: light_account_checks::AccountInfoTrait>( + &'a self, + _accounts: &'a [AI], + _bump_storage: &'a [u8; 1], + ) -> std::result::Result<[&'a [u8]; 3], LightSdkTypesError> { + Err(LightSdkTypesError::InvalidSeeds) + } + + fn into_in_token_data( + &self, + _tree_info: &light_account_pinocchio::PackedStateTreeInfo, + _output_queue_index: u8, + ) -> std::result::Result< + light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext, + LightSdkTypesError, + > { + Err(LightSdkTypesError::InvalidInstructionData) + } + + fn into_in_tlv( + &self, + ) -> std::result::Result< + Option>, + LightSdkTypesError, + > { + Ok(None) + } +} + +// ============================================================================ +// IntoVariant Implementation for AllBorshSeeds (client-side API) +// ============================================================================ + +/// Implement IntoVariant to allow building variant from seeds + compressed data. +/// This enables the high-level `create_load_instructions` API. +#[cfg(not(target_os = "solana"))] +impl light_account_pinocchio::IntoVariant for AllBorshSeeds { + fn into_variant(self, data: &[u8]) -> std::result::Result { + // Deserialize the compressed data (which includes compression_info) + let record: MinimalRecord = + BorshDeserialize::deserialize(&mut &data[..]).map_err(|_| LightSdkTypesError::Borsh)?; + + // Verify the owner in data matches the seed + if record.owner != self.owner { + return Err(LightSdkTypesError::InvalidSeeds); + } + + Ok(AllBorshVariant { + seeds: self, + data: record, + }) + } +} + +// ============================================================================ +// Pack Implementation for AllBorshVariant (client-side API) +// ============================================================================ + +/// Implement Pack trait to allow AllBorshVariant to be used with `create_load_instructions`. +/// Transforms the variant into PackedLightAccountVariant for efficient serialization. +#[cfg(not(target_os = "solana"))] +impl light_account_pinocchio::Pack for AllBorshVariant { + type Packed = crate::derived_variants::PackedLightAccountVariant; + + fn pack( + &self, + accounts: &mut light_account_pinocchio::PackedAccounts, + ) -> std::result::Result { + use light_account_pinocchio::LightAccountVariantTrait; + let (_, bump) = self.derive_pda::(); + let packed_data = self + .data + .pack(accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + Ok( + crate::derived_variants::PackedLightAccountVariant::AllBorsh { + seeds: PackedAllBorshSeeds { + owner_idx: accounts + .insert_or_get(solana_pubkey::Pubkey::from(self.seeds.owner)), + bump, + }, + data: packed_data, + }, + ) + } +} + +// ============================================================================ +// IntoVariant Implementation for AllZeroCopySeeds (client-side API) +// ============================================================================ + +/// Implement IntoVariant to allow building variant from seeds + compressed data. +/// This enables the high-level `create_load_instructions` API. +#[cfg(not(target_os = "solana"))] +impl light_account_pinocchio::IntoVariant for AllZeroCopySeeds { + fn into_variant( + self, + data: &[u8], + ) -> std::result::Result { + // For ZeroCopy (Pod) accounts, data is the full Pod bytes including compression_info. + // We deserialize using BorshDeserialize (which ZeroCopyRecord implements). + let record: ZeroCopyRecord = + BorshDeserialize::deserialize(&mut &data[..]).map_err(|_| LightSdkTypesError::Borsh)?; + + // Verify the owner in data matches the seed + if record.owner != self.owner { + return Err(LightSdkTypesError::InvalidSeeds); + } + + Ok(AllZeroCopyVariant { + seeds: self, + data: record, + }) + } +} + +// ============================================================================ +// Pack Implementation for AllZeroCopyVariant (client-side API) +// ============================================================================ + +/// Implement Pack trait to allow AllZeroCopyVariant to be used with `create_load_instructions`. +/// Transforms the variant into PackedLightAccountVariant for efficient serialization. +#[cfg(not(target_os = "solana"))] +impl light_account_pinocchio::Pack for AllZeroCopyVariant { + type Packed = crate::derived_variants::PackedLightAccountVariant; + + fn pack( + &self, + accounts: &mut light_account_pinocchio::PackedAccounts, + ) -> std::result::Result { + use light_account_pinocchio::LightAccountVariantTrait; + let (_, bump) = self.derive_pda::(); + let packed_data = self + .data + .pack(accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + Ok( + crate::derived_variants::PackedLightAccountVariant::AllZeroCopy { + seeds: PackedAllZeroCopySeeds { + owner_idx: accounts + .insert_or_get(solana_pubkey::Pubkey::from(self.seeds.owner)), + bump, + }, + data: packed_data, + }, + ) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/all/mod.rs b/sdk-tests/pinocchio-manual-test/src/all/mod.rs new file mode 100644 index 0000000000..6117f64308 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/all/mod.rs @@ -0,0 +1,23 @@ +//! All account types creation - manual implementation of macro-generated code. +//! +//! This module demonstrates creating ALL account types in a single instruction: +//! - Borsh PDA (MinimalRecord) +//! - ZeroCopy PDA (ZeroCopyRecord) +//! - Compressed Mint +//! - Token Vault +//! - Associated Token Account (ATA) +//! +//! Key pattern: PDAs + Mints require CPI context flow: +//! - PDAs call `invoke_write_to_cpi_context_first()` (writes to CPI context, doesn't execute) +//! - Mints call `invoke_create_mints()` with `.with_cpi_context_offset(NUM_LIGHT_PDAS)` (executes combined CPI) +//! - Token vault and ATA are separate CPIs (don't participate in CPI context) + +pub mod accounts; +mod derived; +pub mod derived_accounts; + +pub use accounts::*; +pub use derived_accounts::{ + AllBorshSeeds, AllBorshVariant, AllZeroCopySeeds, AllZeroCopyVariant, PackedAllBorshSeeds, + PackedAllBorshVariant, PackedAllZeroCopySeeds, PackedAllZeroCopyVariant, +}; diff --git a/sdk-tests/pinocchio-manual-test/src/ata/accounts.rs b/sdk-tests/pinocchio-manual-test/src/ata/accounts.rs new file mode 100644 index 0000000000..8a4ea5665d --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/ata/accounts.rs @@ -0,0 +1,50 @@ +//! Accounts struct for create_ata instruction (pinocchio version). + +use borsh::{BorshDeserialize, BorshSerialize}; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +/// Params for ATA creation (empty - bump is derived automatically). +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, Default)] +pub struct CreateAtaParams {} + +/// Accounts struct for creating an Associated Token Account. +pub struct CreateAtaAccounts<'a> { + pub payer: &'a AccountInfo, + pub mint: &'a AccountInfo, + pub ata_owner: &'a AccountInfo, + pub user_ata: &'a AccountInfo, + pub compressible_config: &'a AccountInfo, + pub rent_sponsor: &'a AccountInfo, + pub light_token_program: &'a AccountInfo, + pub system_program: &'a AccountInfo, +} + +impl<'a> CreateAtaAccounts<'a> { + pub const FIXED_LEN: usize = 8; + + pub fn parse(accounts: &'a [AccountInfo]) -> Result { + let payer = &accounts[0]; + let mint = &accounts[1]; + let ata_owner = &accounts[2]; + let user_ata = &accounts[3]; + let compressible_config = &accounts[4]; + let rent_sponsor = &accounts[5]; + let light_token_program = &accounts[6]; + let system_program = &accounts[7]; + + if !payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + Ok(Self { + payer, + mint, + ata_owner, + user_ata, + compressible_config, + rent_sponsor, + light_token_program, + system_program, + }) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/ata/derived.rs b/sdk-tests/pinocchio-manual-test/src/ata/derived.rs new file mode 100644 index 0000000000..a11e82cf7e --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/ata/derived.rs @@ -0,0 +1,63 @@ +//! Derived code - what the macro would generate for associated token accounts. + +use light_account_pinocchio::{ + derive_associated_token_account, CreateTokenAtaCpi, LightFinalize, LightPreInit, + LightSdkTypesError, +}; +use pinocchio::account_info::AccountInfo; + +use super::accounts::{CreateAtaAccounts, CreateAtaParams}; + +// ============================================================================ +// LightPreInit Implementation - Creates ATA at START of instruction +// ============================================================================ + +impl LightPreInit for CreateAtaAccounts<'_> { + fn light_pre_init( + &mut self, + _remaining_accounts: &[AccountInfo], + _params: &CreateAtaParams, + ) -> std::result::Result { + let inner = || -> std::result::Result { + // Derive the ATA bump on-chain + let (_, bump) = derive_associated_token_account(self.ata_owner.key(), self.mint.key()); + + // Create ATA via CPI with idempotent + rent-free mode + // NOTE: Unlike token vaults, ATAs use .invoke() not .invoke_signed() + // because ATAs are derived from [owner, token_program, mint], not program PDAs + CreateTokenAtaCpi { + payer: self.payer, + owner: self.ata_owner, + mint: self.mint, + ata: self.user_ata, + bump, + } + .idempotent() // Safe: won't fail if ATA already exists + .rent_free( + self.compressible_config, + self.rent_sponsor, + self.system_program, + ) + .invoke()?; + + // ATAs don't use CPI context, return false + Ok(false) + }; + inner() + } +} + +// ============================================================================ +// LightFinalize Implementation - No-op for ATA only flow +// ============================================================================ + +impl LightFinalize for CreateAtaAccounts<'_> { + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo], + _params: &CreateAtaParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + Ok(()) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/ata/mod.rs b/sdk-tests/pinocchio-manual-test/src/ata/mod.rs new file mode 100644 index 0000000000..55917fe3bf --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/ata/mod.rs @@ -0,0 +1,6 @@ +//! Associated token account creation - manual implementation of macro-generated code. + +pub mod accounts; +mod derived; + +pub use accounts::*; diff --git a/sdk-tests/pinocchio-manual-test/src/derived_compress.rs b/sdk-tests/pinocchio-manual-test/src/derived_compress.rs new file mode 100644 index 0000000000..2f964c5686 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/derived_compress.rs @@ -0,0 +1,77 @@ +//! Macro-derived compress and close implementation. +//! +//! This module contains the code that would be generated by the `#[light_program]` macro. +//! The dispatch function handles type-specific deserialization and compression. + +use borsh::BorshDeserialize; +use light_account_pinocchio::{ + account_meta::CompressedAccountMetaNoLamportsNoAddress, prepare_account_for_compression, + process_compress_pda_accounts_idempotent, CompressCtx, LightDiscriminator, LightSdkTypesError, +}; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +use crate::{account_loader::ZeroCopyRecord, pda::MinimalRecord}; + +/// MACRO-GENERATED: Discriminator-based dispatch function. +/// +/// For each account type, this function: +/// 1. Reads the discriminator from account data +/// 2. Deserializes the account based on discriminator +/// 3. Calls prepare_account_for_compression with the deserialized data +fn compress_dispatch( + account_info: &AccountInfo, + meta: &CompressedAccountMetaNoLamportsNoAddress, + index: usize, + ctx: &mut CompressCtx<'_>, +) -> std::result::Result<(), LightSdkTypesError> { + let data = account_info + .try_borrow_data() + .map_err(|_| LightSdkTypesError::Borsh)?; + + // Read discriminator from first 8 bytes + let discriminator: [u8; 8] = data[..8] + .try_into() + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + + match discriminator { + d if d == MinimalRecord::LIGHT_DISCRIMINATOR => { + // Borsh path: deserialize using try_from_slice + let mut account_data = + MinimalRecord::try_from_slice(&data[8..]).map_err(|_| LightSdkTypesError::Borsh)?; + drop(data); + + // Call prepare with deserialized data + prepare_account_for_compression(account_info, &mut account_data, meta, index, ctx) + } + d if d == ZeroCopyRecord::LIGHT_DISCRIMINATOR => { + // Pod/Zero-copy path: read using bytemuck + // The data is in fixed Pod layout, so we can directly cast it + let record_bytes = &data[8..8 + core::mem::size_of::()]; + let mut account_data: ZeroCopyRecord = *bytemuck::from_bytes(record_bytes); + drop(data); + + // Same prepare function works - hashing uses try_to_vec() which ZeroCopyRecord supports + // via its BorshSerialize implementation + prepare_account_for_compression(account_info, &mut account_data, meta, index, ctx) + } + // Unknown discriminator - skip (not an error, could be different account type) + _ => Ok(()), + } +} + +/// MACRO-GENERATED: Process handler - deserializes params and forwards to SDK function. +pub fn process_compress_and_close( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), ProgramError> { + let params = light_account_pinocchio::CompressAndCloseParams::try_from_slice(instruction_data) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_compress_pda_accounts_idempotent( + accounts, + ¶ms, + compress_dispatch, + crate::LIGHT_CPI_SIGNER, + &crate::ID, + ) + .map_err(|e| ProgramError::Custom(u32::from(e))) +} diff --git a/sdk-tests/pinocchio-manual-test/src/derived_decompress.rs b/sdk-tests/pinocchio-manual-test/src/derived_decompress.rs new file mode 100644 index 0000000000..15ee429cb7 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/derived_decompress.rs @@ -0,0 +1,34 @@ +//! Macro-derived decompress implementation. +//! +//! This module contains the code that would be generated by the `#[light_program]` macro. +//! With the trait-based dispatch, this module is minimal - just specifies the variant type. + +use light_account_pinocchio::process_decompress_pda_accounts_idempotent; +use pinocchio::{ + account_info::AccountInfo, + program_error::ProgramError, + sysvars::{clock::Clock, Sysvar}, +}; + +use crate::derived_variants::PackedLightAccountVariant; + +/// MACRO-GENERATED: Process handler - deserializes params and forwards to SDK function. +pub fn process_decompress_idempotent( + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), ProgramError> { + use borsh::BorshDeserialize; + let params = light_account_pinocchio::DecompressIdempotentParams::::try_from_slice(instruction_data) + .map_err(|_| ProgramError::InvalidInstructionData)?; + let current_slot = Clock::get() + .map_err(|_| ProgramError::UnsupportedSysvar)? + .slot; + process_decompress_pda_accounts_idempotent::<_, PackedLightAccountVariant>( + accounts, + ¶ms, + crate::LIGHT_CPI_SIGNER, + &crate::ID, + current_slot, + ) + .map_err(|e| ProgramError::Custom(u32::from(e))) +} diff --git a/sdk-tests/pinocchio-manual-test/src/derived_light_config.rs b/sdk-tests/pinocchio-manual-test/src/derived_light_config.rs new file mode 100644 index 0000000000..def854e2a6 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/derived_light_config.rs @@ -0,0 +1,63 @@ +//! Config instructions using SDK functions. + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{process_initialize_light_config, process_update_light_config}; +use light_compressible::rent::RentConfig; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +/// Params order matches SDK's InitializeCompressionConfigAnchorData. +#[derive(BorshSerialize, BorshDeserialize, Clone)] +pub struct InitConfigParams { + pub write_top_up: u32, + pub rent_sponsor: [u8; 32], + pub compression_authority: [u8; 32], + pub rent_config: RentConfig, + pub address_space: Vec<[u8; 32]>, +} + +/// Account order matches SDK's InitializeRentFreeConfig::build(). +/// Order: [payer, config, program_data, authority, system_program] +pub fn process_initialize_config( + accounts: &[AccountInfo], + data: &[u8], +) -> Result<(), ProgramError> { + let params = InitConfigParams::try_from_slice(data).map_err(|_| ProgramError::BorshIoError)?; + + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let fee_payer = &accounts[0]; + let config = &accounts[1]; + let _program_data = &accounts[2]; + let authority = &accounts[3]; + let system_program = &accounts[4]; + + process_initialize_light_config( + config, + authority, + ¶ms.rent_sponsor, + ¶ms.compression_authority, + params.rent_config, + params.write_top_up, + params.address_space, + 0, // config_bump + fee_payer, + system_program, + &crate::ID, + ) + .map_err(|e| ProgramError::Custom(u32::from(e))) +} + +pub fn process_update_config(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + if accounts.len() < 2 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let authority = &accounts[0]; + let config = &accounts[1]; + + let remaining = [*config, *authority]; + process_update_light_config(&remaining, data, &crate::ID) + .map_err(|e| ProgramError::Custom(u32::from(e))) +} diff --git a/sdk-tests/pinocchio-manual-test/src/derived_variants.rs b/sdk-tests/pinocchio-manual-test/src/derived_variants.rs new file mode 100644 index 0000000000..6aebc1971f --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/derived_variants.rs @@ -0,0 +1,143 @@ +//! Program-wide variant enums for compress/decompress dispatch. +//! +//! This module contains the code that would be generated by the `#[light_program]` macro. + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{ + prepare_account_for_decompression, DecompressCtx, DecompressVariant, LightSdkTypesError, + PackedStateTreeInfo, +}; +use pinocchio::account_info::AccountInfo; + +use crate::{ + account_loader::derived_accounts::{ + PackedZeroCopyRecordSeeds, PackedZeroCopyRecordVariant, ZeroCopyRecordSeeds, + }, + all::derived_accounts::{ + AllBorshSeeds, AllZeroCopySeeds, PackedAllBorshSeeds, PackedAllBorshVariant, + PackedAllZeroCopySeeds, PackedAllZeroCopyVariant, + }, + pda::derived_accounts::{ + MinimalRecordSeeds, PackedMinimalRecordSeeds, PackedMinimalRecordVariant, + }, + MinimalRecord, PackedMinimalRecord, PackedZeroCopyRecord, ZeroCopyRecord, +}; + +// ============================================================================ +// Program-wide Variant Enums (generated by #[light_program]) +// ============================================================================ + +/// Unpacked variant enum for all account types in this program. +/// Each variant contains the full seeds + data. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub enum LightAccountVariant { + MinimalRecord { + seeds: MinimalRecordSeeds, + data: MinimalRecord, + }, + ZeroCopyRecord { + seeds: ZeroCopyRecordSeeds, + data: ZeroCopyRecord, + }, + AllBorsh { + seeds: AllBorshSeeds, + data: MinimalRecord, + }, + AllZeroCopy { + seeds: AllZeroCopySeeds, + data: ZeroCopyRecord, + }, +} + +/// Packed variant enum for efficient serialization. +/// Does NOT wrap CompressedAccountData - that wrapper is added by the client library. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub enum PackedLightAccountVariant { + MinimalRecord { + seeds: PackedMinimalRecordSeeds, + data: PackedMinimalRecord, + }, + ZeroCopyRecord { + seeds: PackedZeroCopyRecordSeeds, + data: PackedZeroCopyRecord, + }, + AllBorsh { + seeds: PackedAllBorshSeeds, + data: PackedMinimalRecord, + }, + AllZeroCopy { + seeds: PackedAllZeroCopySeeds, + data: PackedZeroCopyRecord, + }, +} + +// ============================================================================ +// DecompressVariant Implementation (MACRO-GENERATED) +// ============================================================================ + +/// Implementation for PackedLightAccountVariant. +/// Implements on the inner variant type to satisfy orphan rules. +impl DecompressVariant for PackedLightAccountVariant { + fn decompress( + &self, + tree_info: &PackedStateTreeInfo, + pda_account: &AccountInfo, + ctx: &mut DecompressCtx<'_>, + ) -> std::result::Result<(), LightSdkTypesError> { + let output_queue_index = ctx.output_queue_index; + match self { + PackedLightAccountVariant::MinimalRecord { seeds, data } => { + let packed_data = PackedMinimalRecordVariant { + seeds: seeds.clone(), + data: data.clone(), + }; + prepare_account_for_decompression::<4, PackedMinimalRecordVariant, AccountInfo>( + &packed_data, + tree_info, + output_queue_index, + pda_account, + ctx, + ) + } + PackedLightAccountVariant::ZeroCopyRecord { seeds, data } => { + let packed_data = PackedZeroCopyRecordVariant { + seeds: seeds.clone(), + data: data.clone(), + }; + prepare_account_for_decompression::<4, PackedZeroCopyRecordVariant, AccountInfo>( + &packed_data, + tree_info, + output_queue_index, + pda_account, + ctx, + ) + } + PackedLightAccountVariant::AllBorsh { seeds, data } => { + let packed_data = PackedAllBorshVariant { + seeds: seeds.clone(), + data: data.clone(), + }; + prepare_account_for_decompression::<3, PackedAllBorshVariant, AccountInfo>( + &packed_data, + tree_info, + output_queue_index, + pda_account, + ctx, + ) + } + PackedLightAccountVariant::AllZeroCopy { seeds, data } => { + let packed_data = PackedAllZeroCopyVariant { + seeds: seeds.clone(), + data: data.clone(), + }; + prepare_account_for_decompression::<3, PackedAllZeroCopyVariant, AccountInfo>( + &packed_data, + tree_info, + output_queue_index, + pda_account, + ctx, + ) + } + } + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/lib.rs b/sdk-tests/pinocchio-manual-test/src/lib.rs new file mode 100644 index 0000000000..cfcfbaa696 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/lib.rs @@ -0,0 +1,280 @@ +//! Pinocchio-based test program for compressible PDA creation. +//! +//! This is a pinocchio port of the manual-test program. +//! Same instructions, same behavior, no Anchor dependency. + +#![allow(deprecated)] + +use light_account_pinocchio::{derive_light_cpi_signer, CpiSigner, LightFinalize, LightPreInit}; +use light_macros::pubkey_array; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +pub mod account_loader; +pub mod all; +pub mod ata; +pub mod derived_compress; +pub mod derived_decompress; +pub mod derived_light_config; +pub mod derived_variants; +pub mod pda; +pub mod token_account; +pub mod two_mints; + +// Re-exports for tests and other consumers +pub use account_loader::{ + PackedZeroCopyRecord, PackedZeroCopyRecordSeeds, PackedZeroCopyRecordVariant, ZeroCopyRecord, + ZeroCopyRecordSeeds, ZeroCopyRecordVariant, +}; +pub use all::{ + AllBorshSeeds, AllBorshVariant, AllZeroCopySeeds, AllZeroCopyVariant, PackedAllBorshSeeds, + PackedAllBorshVariant, PackedAllZeroCopySeeds, PackedAllZeroCopyVariant, +}; +pub use ata::accounts::*; +pub use derived_variants::{LightAccountVariant, PackedLightAccountVariant}; +pub use light_account_pinocchio::{ + AccountType, CompressAndCloseParams, DecompressIdempotentParams, DecompressVariant, + LightAccount, +}; +pub use pda::{ + MinimalRecord, MinimalRecordSeeds, MinimalRecordVariant, PackedMinimalRecord, + PackedMinimalRecordSeeds, PackedMinimalRecordVariant, +}; +pub use token_account::accounts::*; +pub use two_mints::accounts::*; + +pub const ID: Pubkey = pubkey_array!("7TWLq8Kmj1Cc3bGaEqsdNKMAiJSA7XN1JeKCN5nQeg2R"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("7TWLq8Kmj1Cc3bGaEqsdNKMAiJSA7XN1JeKCN5nQeg2R"); + +// ============================================================================ +// Instruction Discriminators (8-byte, Anchor-compatible via sha256("global:{name}")[..8]) +// ============================================================================ +pub mod discriminators { + pub const CREATE_PDA: [u8; 8] = [220, 10, 244, 120, 183, 4, 64, 232]; + pub const CREATE_ZERO_COPY: [u8; 8] = [172, 231, 175, 212, 64, 240, 20, 209]; + pub const CREATE_DERIVED_MINTS: [u8; 8] = [91, 123, 65, 133, 194, 45, 243, 75]; + pub const CREATE_TOKEN_VAULT: [u8; 8] = [161, 29, 12, 45, 127, 88, 61, 49]; + pub const CREATE_ATA: [u8; 8] = [26, 102, 168, 62, 117, 72, 168, 17]; + pub const CREATE_ALL: [u8; 8] = [149, 49, 144, 45, 208, 155, 177, 43]; + // These match the hardcoded discriminators in light_client::interface::instructions + pub const INITIALIZE_COMPRESSION_CONFIG: [u8; 8] = [133, 228, 12, 169, 56, 76, 222, 61]; + pub const UPDATE_COMPRESSION_CONFIG: [u8; 8] = [135, 215, 243, 81, 163, 146, 33, 70]; + pub const COMPRESS_ACCOUNTS_IDEMPOTENT: [u8; 8] = [70, 236, 171, 120, 164, 93, 113, 181]; + pub const DECOMPRESS_ACCOUNTS_IDEMPOTENT: [u8; 8] = [114, 67, 61, 123, 234, 31, 1, 112]; +} + +// ============================================================================ +// Entrypoint +// ============================================================================ + +pinocchio::entrypoint!(process_instruction); + +pub fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> Result<(), ProgramError> { + if instruction_data.len() < 8 { + return Err(ProgramError::InvalidInstructionData); + } + + let (disc, data) = instruction_data.split_at(8); + let disc: [u8; 8] = disc.try_into().unwrap(); + + match disc { + discriminators::CREATE_PDA => process_create_pda(accounts, data), + discriminators::CREATE_ZERO_COPY => process_create_zero_copy(accounts, data), + discriminators::CREATE_DERIVED_MINTS => process_create_derived_mints(accounts, data), + discriminators::CREATE_TOKEN_VAULT => process_create_token_vault(accounts, data), + discriminators::CREATE_ATA => process_create_ata(accounts, data), + discriminators::CREATE_ALL => process_create_all(accounts, data), + discriminators::INITIALIZE_COMPRESSION_CONFIG => { + derived_light_config::process_initialize_config(accounts, data) + } + discriminators::UPDATE_COMPRESSION_CONFIG => { + derived_light_config::process_update_config(accounts, data) + } + discriminators::COMPRESS_ACCOUNTS_IDEMPOTENT => { + derived_compress::process_compress_and_close(accounts, data) + } + discriminators::DECOMPRESS_ACCOUNTS_IDEMPOTENT => { + derived_decompress::process_decompress_idempotent(accounts, data) + } + _ => Err(ProgramError::InvalidInstructionData), + } +} + +// ============================================================================ +// Instruction Handlers +// ============================================================================ + +fn process_create_pda(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use borsh::BorshDeserialize; + use pda::accounts::{CreatePda, CreatePdaParams}; + + let params = + CreatePdaParams::deserialize(&mut &data[..]).map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreatePda::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let mut ctx = CreatePda::parse(fixed_accounts, ¶ms)?; + + let has_pre_init = ctx + .light_pre_init(remaining_accounts, ¶ms) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + // Business logic: set account data + { + let mut account_data = ctx + .record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + let record = pda::state::MinimalRecord::mut_from_account_data(&mut account_data); + record.owner = params.owner; + } + + ctx.light_finalize(remaining_accounts, ¶ms, has_pre_init) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_zero_copy(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use account_loader::accounts::{CreateZeroCopy, CreateZeroCopyParams}; + use borsh::BorshDeserialize; + + let params = CreateZeroCopyParams::deserialize(&mut &data[..]) + .map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateZeroCopy::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let mut ctx = CreateZeroCopy::parse(fixed_accounts, ¶ms)?; + + let has_pre_init = ctx + .light_pre_init(remaining_accounts, ¶ms) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + // Business logic: set zero-copy account data + { + let mut account_data = ctx + .record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + let record_bytes = + &mut account_data[8..8 + core::mem::size_of::()]; + let record: &mut account_loader::ZeroCopyRecord = bytemuck::from_bytes_mut(record_bytes); + record.owner = params.owner; + record.value = params.value; + } + + ctx.light_finalize(remaining_accounts, ¶ms, has_pre_init) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_derived_mints(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use borsh::BorshDeserialize; + use two_mints::accounts::{CreateDerivedMintsAccounts, CreateDerivedMintsParams}; + + let params = CreateDerivedMintsParams::deserialize(&mut &data[..]) + .map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateDerivedMintsAccounts::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let mut ctx = CreateDerivedMintsAccounts::parse(fixed_accounts, ¶ms)?; + + let has_pre_init = ctx + .light_pre_init(remaining_accounts, ¶ms) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + ctx.light_finalize(remaining_accounts, ¶ms, has_pre_init) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_token_vault(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use borsh::BorshDeserialize; + use token_account::accounts::{CreateTokenVaultAccounts, CreateTokenVaultParams}; + + let params = CreateTokenVaultParams::deserialize(&mut &data[..]) + .map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateTokenVaultAccounts::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let mut ctx = CreateTokenVaultAccounts::parse(fixed_accounts)?; + + let has_pre_init = ctx + .light_pre_init(remaining_accounts, ¶ms) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + ctx.light_finalize(remaining_accounts, ¶ms, has_pre_init) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_ata(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use ata::accounts::{CreateAtaAccounts, CreateAtaParams}; + use borsh::BorshDeserialize; + + let params = + CreateAtaParams::deserialize(&mut &data[..]).map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateAtaAccounts::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let mut ctx = CreateAtaAccounts::parse(fixed_accounts)?; + + let has_pre_init = ctx + .light_pre_init(remaining_accounts, ¶ms) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + ctx.light_finalize(remaining_accounts, ¶ms, has_pre_init) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} + +fn process_create_all(accounts: &[AccountInfo], data: &[u8]) -> Result<(), ProgramError> { + use all::accounts::{CreateAllAccounts, CreateAllParams}; + use borsh::BorshDeserialize; + + let params = + CreateAllParams::deserialize(&mut &data[..]).map_err(|_| ProgramError::BorshIoError)?; + + let remaining_start = CreateAllAccounts::FIXED_LEN; + let (fixed_accounts, remaining_accounts) = accounts.split_at(remaining_start); + let mut ctx = CreateAllAccounts::parse(fixed_accounts, ¶ms)?; + + let has_pre_init = ctx + .light_pre_init(remaining_accounts, ¶ms) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + // Business logic: set PDA data + { + let mut borsh_data = ctx + .borsh_record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + let borsh_record = pda::state::MinimalRecord::mut_from_account_data(&mut borsh_data); + borsh_record.owner = params.owner; + } + { + let mut zc_data = ctx + .zero_copy_record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + let record_bytes = + &mut zc_data[8..8 + core::mem::size_of::()]; + let record: &mut account_loader::ZeroCopyRecord = bytemuck::from_bytes_mut(record_bytes); + record.owner = params.owner; + record.value = params.value; + } + + ctx.light_finalize(remaining_accounts, ¶ms, has_pre_init) + .map_err(|e| ProgramError::Custom(u32::from(e)))?; + + Ok(()) +} diff --git a/sdk-tests/pinocchio-manual-test/src/pda/accounts.rs b/sdk-tests/pinocchio-manual-test/src/pda/accounts.rs new file mode 100644 index 0000000000..6e798e3223 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/pda/accounts.rs @@ -0,0 +1,91 @@ +//! Accounts module for single-pda-test (pinocchio version). + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::CreateAccountsProof; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, + sysvars::Sysvar, +}; + +use super::state::MinimalRecord; + +#[derive(BorshSerialize, BorshDeserialize, Clone)] +pub struct CreatePdaParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: [u8; 32], + pub nonce: u64, +} + +/// Minimal accounts struct for testing single PDA creation. +pub struct CreatePda<'a> { + pub fee_payer: &'a AccountInfo, + pub compression_config: &'a AccountInfo, + pub record: &'a AccountInfo, + pub system_program: &'a AccountInfo, +} + +impl<'a> CreatePda<'a> { + pub const FIXED_LEN: usize = 4; + + pub fn parse( + accounts: &'a [AccountInfo], + params: &CreatePdaParams, + ) -> Result { + let fee_payer = &accounts[0]; + let compression_config = &accounts[1]; + let record = &accounts[2]; + let system_program = &accounts[3]; + + if !fee_payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Verify and create the PDA account via system program CPI + let space = 8 + MinimalRecord::INIT_SPACE; + let nonce_bytes = params.nonce.to_le_bytes(); + let seeds: &[&[u8]] = &[b"minimal_record", ¶ms.owner, &nonce_bytes]; + let (expected_pda, bump) = pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if record.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + + let rent = + pinocchio::sysvars::rent::Rent::get().map_err(|_| ProgramError::UnsupportedSysvar)?; + let lamports = rent.minimum_balance(space); + + let bump_bytes = [bump]; + let seed_array = [ + Seed::from(b"minimal_record" as &[u8]), + Seed::from(params.owner.as_ref()), + Seed::from(nonce_bytes.as_ref()), + Seed::from(bump_bytes.as_ref()), + ]; + let signer = Signer::from(&seed_array); + pinocchio_system::instructions::CreateAccount { + from: fee_payer, + to: record, + lamports, + space: space as u64, + owner: &crate::ID, + } + .invoke_signed(&[signer])?; + + // Write LIGHT_DISCRIMINATOR to first 8 bytes + { + use light_account_pinocchio::LightDiscriminator; + let mut data = record + .try_borrow_mut_data() + .map_err(|_| ProgramError::AccountBorrowFailed)?; + data[..8].copy_from_slice(&MinimalRecord::LIGHT_DISCRIMINATOR); + } + + Ok(Self { + fee_payer, + compression_config, + record, + system_program, + }) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/pda/derived_accounts.rs b/sdk-tests/pinocchio-manual-test/src/pda/derived_accounts.rs new file mode 100644 index 0000000000..f5dbd70139 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/pda/derived_accounts.rs @@ -0,0 +1,375 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{ + light_account_checks::{self, packed_accounts::ProgramPackedAccounts}, + prepare_compressed_account_on_init, CpiAccounts, CpiAccountsConfig, CpiContextWriteAccounts, + InvokeLightSystemProgram, LightAccount, LightAccountVariantTrait, LightFinalize, LightPreInit, + LightSdkTypesError, PackedAddressTreeInfoExt, PackedLightAccountVariantTrait, +}; +use light_compressed_account::instruction_data::{ + cpi_context::CompressedCpiContext, with_account_info::InstructionDataInvokeCpiWithAccountInfo, +}; +use pinocchio::account_info::AccountInfo; + +use super::{ + accounts::{CreatePda, CreatePdaParams}, + derived_state::PackedMinimalRecord, + state::MinimalRecord, +}; + +// ============================================================================ +// Compile-time Size Validation (800-byte limit for compressed accounts) +// ============================================================================ + +const _: () = { + const COMPRESSED_SIZE: usize = 8 + MinimalRecord::INIT_SPACE; + assert!( + COMPRESSED_SIZE <= 800, + "Compressed account 'MinimalRecord' exceeds 800-byte compressible account size limit" + ); +}; + +// ============================================================================ +// Manual LightPreInit Implementation +// ============================================================================ + +impl LightPreInit for CreatePda<'_> { + fn light_pre_init( + &mut self, + remaining_accounts: &[AccountInfo], + params: &CreatePdaParams, + ) -> std::result::Result { + let inner = || -> std::result::Result { + use light_account_pinocchio::{LightAccount, LightConfig}; + use pinocchio::sysvars::{clock::Clock, Sysvar}; + + // 1. Build CPI accounts (slice remaining_accounts at system_accounts_offset) + let system_accounts_offset = + params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + self.fee_payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + // 2. Get address tree pubkey from packed tree info + let address_tree_info = ¶ms.create_accounts_proof.address_tree_info; + let address_tree_pubkey = address_tree_info + .get_tree_pubkey(&cpi_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let output_tree_index = params.create_accounts_proof.output_state_tree_index; + let current_account_index: u8 = 0; + // Is true if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. + const WITH_CPI_CONTEXT: bool = false; + + const NUM_LIGHT_PDAS: usize = 1; + + // 6. Set compression_info from config + let light_config = LightConfig::load_checked(self.compression_config, &crate::ID) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + let current_slot = Clock::get() + .map_err(|_| LightSdkTypesError::InvalidInstructionData)? + .slot; + // Dynamic derived light pda specific. Only exists if NUM_LIGHT_PDAS > 0 + // ===================================================================== + { + // Is first if the instruction creates 1 or more light mints in addition to 1 or more light pda accounts. + let cpi_context = if WITH_CPI_CONTEXT { + CompressedCpiContext::first() + } else { + CompressedCpiContext::default() + }; + let mut new_address_params = Vec::with_capacity(NUM_LIGHT_PDAS); + let mut account_infos = Vec::with_capacity(NUM_LIGHT_PDAS); + // 3. Prepare compressed account using helper function + // Dynamic code 0-N variants depending on the accounts struct + // ===================================================================== + let record_key = *self.record.key(); + prepare_compressed_account_on_init( + &record_key, + &address_tree_pubkey, + address_tree_info, + output_tree_index, + current_account_index, + &crate::ID, + &mut new_address_params, + &mut account_infos, + )?; + // Set compression_info on the Borsh record via mut_from_account_data + { + let mut account_data = self + .record + .try_borrow_mut_data() + .map_err(|_| LightSdkTypesError::Borsh)?; + let record = MinimalRecord::mut_from_account_data(&mut account_data); + record.set_decompressed(&light_config, current_slot); + } + // ===================================================================== + + // current_account_index += 1; + // For multiple accounts, repeat the pattern: + // let prepared2 = prepare_compressed_account_on_init(..., current_account_index, ...)?; + // current_account_index += 1; + + // 4. Build instruction data manually (no builder pattern) + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, // V2 mode + bump: crate::LIGHT_CPI_SIGNER.bump, + invoking_program_id: crate::LIGHT_CPI_SIGNER.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: WITH_CPI_CONTEXT, + with_transaction_hash: false, + cpi_context, + proof: params.create_accounts_proof.proof.0, + new_address_params, + account_infos, + read_only_addresses: vec![], + read_only_accounts: vec![], + }; + if !WITH_CPI_CONTEXT { + // 5. Invoke Light System Program CPI + instruction_data.invoke(cpi_accounts)?; + } else { + // For flows that combine light mints with light PDAs, write to CPI context first. + // The authority and cpi_context accounts must be provided in remaining_accounts. + let cpi_context_accounts = CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority()?, + cpi_context: cpi_accounts.cpi_context()?, + cpi_signer: crate::LIGHT_CPI_SIGNER, + }; + instruction_data.invoke_write_to_cpi_context_first(cpi_context_accounts)?; + } + } + // ===================================================================== + Ok(false) // No mints, so no CPI context write + }; + inner() + } +} + +// ============================================================================ +// Manual LightFinalize Implementation (no-op for PDA-only flow) +// ============================================================================ + +impl LightFinalize for CreatePda<'_> { + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo], + _params: &CreatePdaParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + // No-op for PDA-only flow - compression CPI already executed in light_pre_init + Ok(()) + } +} + +// ============================================================================ +// Seeds Structs +// Extracted from: seeds = [b"minimal_record", params.owner.as_ref()] +// ============================================================================ + +/// Seeds for MinimalRecord PDA. +/// Contains the dynamic seed values (static prefix "minimal_record" is in seed_refs). +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct MinimalRecordSeeds { + pub owner: [u8; 32], + pub nonce: u64, +} + +/// Packed seeds with u8 indices instead of Pubkeys. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedMinimalRecordSeeds { + pub owner_idx: u8, + pub nonce_bytes: [u8; 8], + pub bump: u8, +} + +// ============================================================================ +// Variant Structs +// ============================================================================ + +/// Full variant combining seeds + data. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct MinimalRecordVariant { + pub seeds: MinimalRecordSeeds, + pub data: MinimalRecord, +} + +/// Packed variant for efficient serialization. +/// Contains packed seeds and data with u8 indices for Pubkey deduplication. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedMinimalRecordVariant { + pub seeds: PackedMinimalRecordSeeds, + pub data: PackedMinimalRecord, +} + +// ============================================================================ +// LightAccountVariant Implementation +// ============================================================================ + +impl LightAccountVariantTrait<4> for MinimalRecordVariant { + const PROGRAM_ID: [u8; 32] = crate::ID; + + type Seeds = MinimalRecordSeeds; + type Data = MinimalRecord; + type Packed = PackedMinimalRecordVariant; + + fn data(&self) -> &Self::Data { + &self.data + } + + /// Get seed values as owned byte vectors for PDA derivation. + /// Generated from: seeds = [b"minimal_record", params.owner.as_ref(), ¶ms.nonce.to_le_bytes()] + fn seed_vec(&self) -> Vec> { + vec![ + b"minimal_record".to_vec(), + self.seeds.owner.to_vec(), + self.seeds.nonce.to_le_bytes().to_vec(), + ] + } + + /// Get seed references with bump for CPI signing. + /// Note: For unpacked variants with computed bytes (like nonce.to_le_bytes()), + /// we cannot return references to temporaries. Use the packed variant instead. + fn seed_refs_with_bump<'a>(&'a self, _bump_storage: &'a [u8; 1]) -> [&'a [u8]; 4] { + // The packed variant stores nonce_bytes as [u8; 8], so it can return references. + // This unpacked variant computes nonce.to_le_bytes() which creates a temporary. + panic!("Use PackedMinimalRecordVariant::seed_refs_with_bump instead") + } +} + +// ============================================================================ +// PackedLightAccountVariant Implementation +// ============================================================================ + +impl PackedLightAccountVariantTrait<4> for PackedMinimalRecordVariant { + type Unpacked = MinimalRecordVariant; + + const ACCOUNT_TYPE: light_account_pinocchio::AccountType = + ::ACCOUNT_TYPE; + + fn bump(&self) -> u8 { + self.seeds.bump + } + + fn unpack( + &self, + accounts: &[AI], + ) -> std::result::Result { + let owner = accounts + .get(self.seeds.owner_idx as usize) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)?; + + // Build ProgramPackedAccounts for LightAccount::unpack + let packed_accounts = ProgramPackedAccounts { accounts }; + let data = MinimalRecord::unpack(&self.data, &packed_accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + + Ok(MinimalRecordVariant { + seeds: MinimalRecordSeeds { + owner: owner.key(), + nonce: u64::from_le_bytes(self.seeds.nonce_bytes), + }, + data, + }) + } + + fn seed_refs_with_bump<'a, AI: light_account_checks::AccountInfoTrait>( + &'a self, + _accounts: &'a [AI], + _bump_storage: &'a [u8; 1], + ) -> std::result::Result<[&'a [u8]; 4], LightSdkTypesError> { + // PDA variants use seed_vec() in the decompression path, not seed_refs_with_bump. + // Returning a reference to the account key requires a key_ref() method on + // AccountInfoTrait, which is not yet available. Since this method is only + // called for token account variants, PDA variants return an error. + Err(LightSdkTypesError::InvalidSeeds) + } + + fn into_in_token_data( + &self, + _tree_info: &light_account_pinocchio::PackedStateTreeInfo, + _output_queue_index: u8, + ) -> std::result::Result< + light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext, + LightSdkTypesError, + > { + Err(LightSdkTypesError::InvalidInstructionData) + } + + fn into_in_tlv( + &self, + ) -> std::result::Result< + Option>, + LightSdkTypesError, + > { + Ok(None) + } +} + +// ============================================================================ +// IntoVariant Implementation for Seeds (client-side API) +// ============================================================================ + +/// Implement IntoVariant to allow building variant from seeds + compressed data. +/// This enables the high-level `create_load_instructions` API. +#[cfg(not(target_os = "solana"))] +impl light_account_pinocchio::IntoVariant for MinimalRecordSeeds { + fn into_variant( + self, + data: &[u8], + ) -> std::result::Result { + // Deserialize the compressed data (which includes compression_info) + let record: MinimalRecord = + BorshDeserialize::deserialize(&mut &data[..]).map_err(|_| LightSdkTypesError::Borsh)?; + + // Verify the owner in data matches the seed + if record.owner != self.owner { + return Err(LightSdkTypesError::InvalidSeeds); + } + + Ok(MinimalRecordVariant { + seeds: self, + data: record, + }) + } +} + +// ============================================================================ +// Pack Implementation for MinimalRecordVariant (client-side API) +// ============================================================================ + +/// Implement Pack trait to allow MinimalRecordVariant to be used with `create_load_instructions`. +/// Transforms the variant into PackedLightAccountVariant for efficient serialization. +#[cfg(not(target_os = "solana"))] +impl light_account_pinocchio::Pack for MinimalRecordVariant { + type Packed = crate::derived_variants::PackedLightAccountVariant; + + fn pack( + &self, + accounts: &mut light_account_pinocchio::PackedAccounts, + ) -> std::result::Result { + use light_account_pinocchio::LightAccountVariantTrait; + let (_, bump) = self.derive_pda::(); + let packed_data = self + .data + .pack(accounts) + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + Ok( + crate::derived_variants::PackedLightAccountVariant::MinimalRecord { + seeds: PackedMinimalRecordSeeds { + owner_idx: accounts + .insert_or_get(solana_pubkey::Pubkey::from(self.seeds.owner)), + nonce_bytes: self.seeds.nonce.to_le_bytes(), + bump, + }, + data: packed_data, + }, + ) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/pda/derived_state.rs b/sdk-tests/pinocchio-manual-test/src/pda/derived_state.rs new file mode 100644 index 0000000000..e48569ba70 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/pda/derived_state.rs @@ -0,0 +1,94 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{ + light_account_checks::{packed_accounts::ProgramPackedAccounts, AccountInfoTrait}, + AccountType, CompressionInfo, HasCompressionInfo, LightAccount, LightConfig, + LightSdkTypesError, +}; + +use super::state::MinimalRecord; + +// ============================================================================ +// PackedMinimalRecord (compression_info excluded per implementation_details.md) +// ============================================================================ + +/// Packed version of MinimalRecord for efficient transmission. +/// compression_info is excluded - it's cut off during pack. +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedMinimalRecord { + /// Index into remaining_accounts instead of full Pubkey + pub owner: u8, +} + +// ============================================================================ +// LightAccount Implementation for MinimalRecord +// ============================================================================ + +impl LightAccount for MinimalRecord { + const ACCOUNT_TYPE: AccountType = AccountType::Pda; + + type Packed = PackedMinimalRecord; + + // CompressionInfo (24) + owner (32) = 56 bytes + const INIT_SPACE: usize = core::mem::size_of::() + 32; + + fn compression_info(&self) -> &CompressionInfo { + &self.compression_info + } + + fn compression_info_mut(&mut self) -> &mut CompressionInfo { + &mut self.compression_info + } + + fn set_decompressed(&mut self, config: &LightConfig, current_slot: u64) { + self.compression_info = CompressionInfo::new_from_config(config, current_slot); + } + + #[cfg(not(target_os = "solana"))] + fn pack( + &self, + accounts: &mut light_account_pinocchio::interface::instruction::PackedAccounts, + ) -> std::result::Result { + // compression_info excluded from packed struct + Ok(PackedMinimalRecord { + owner: accounts.insert_or_get(AM::pubkey_from_bytes(self.owner)), + }) + } + + fn unpack( + packed: &Self::Packed, + accounts: &ProgramPackedAccounts, + ) -> std::result::Result { + // Use get_u8 with a descriptive name for better error messages + let owner_account = accounts + .get_u8(packed.owner, "MinimalRecord: owner") + .map_err(|_| LightSdkTypesError::InvalidInstructionData)?; + + // Set compression_info to compressed() for hash verification at decompress + // key() returns [u8; 32] directly - no Pubkey::from() needed + Ok(MinimalRecord { + compression_info: CompressionInfo::compressed(), + owner: owner_account.key(), + }) + } +} + +impl HasCompressionInfo for MinimalRecord { + fn compression_info(&self) -> std::result::Result<&CompressionInfo, LightSdkTypesError> { + Ok(&self.compression_info) + } + + fn compression_info_mut( + &mut self, + ) -> std::result::Result<&mut CompressionInfo, LightSdkTypesError> { + Ok(&mut self.compression_info) + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + panic!("compression_info_mut_opt not supported for LightAccount types (use compression_info_mut instead)") + } + + fn set_compression_info_none(&mut self) -> std::result::Result<(), LightSdkTypesError> { + self.compression_info = CompressionInfo::compressed(); + Ok(()) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/pda/mod.rs b/sdk-tests/pinocchio-manual-test/src/pda/mod.rs new file mode 100644 index 0000000000..30965d2e2c --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/pda/mod.rs @@ -0,0 +1,13 @@ +//! PDA state and accounts for manual Light Protocol implementation. + +pub mod accounts; +pub mod derived_accounts; +pub mod derived_state; +pub mod state; + +pub use accounts::*; +pub use derived_accounts::{ + MinimalRecordSeeds, MinimalRecordVariant, PackedMinimalRecordSeeds, PackedMinimalRecordVariant, +}; +pub use derived_state::*; +pub use state::*; diff --git a/sdk-tests/pinocchio-manual-test/src/pda/state.rs b/sdk-tests/pinocchio-manual-test/src/pda/state.rs new file mode 100644 index 0000000000..2b88a96fa1 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/pda/state.rs @@ -0,0 +1,27 @@ +//! State module for single-pda-test. + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::{CompressionInfo, LightDiscriminator, LightHasherSha}; + +/// Minimal record struct for testing PDA creation. +#[derive( + Default, Debug, Clone, BorshSerialize, BorshDeserialize, LightDiscriminator, LightHasherSha, +)] +#[repr(C)] +pub struct MinimalRecord { + pub compression_info: CompressionInfo, + pub owner: [u8; 32], +} + +impl MinimalRecord { + pub const INIT_SPACE: usize = core::mem::size_of::() + 32; + + /// Get a mutable reference to a MinimalRecord from account data (after 8-byte discriminator). + pub fn mut_from_account_data(data: &mut [u8]) -> &mut Self { + let start = 8; // skip discriminator + let end = start + Self::INIT_SPACE; + // Safety: MinimalRecord is just bytes (CompressionInfo is 24 bytes + [u8;32]) + // We need to do manual byte access since it's Borsh-serialized + unsafe { &mut *(data[start..end].as_mut_ptr() as *mut Self) } + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/token_account/accounts.rs b/sdk-tests/pinocchio-manual-test/src/token_account/accounts.rs new file mode 100644 index 0000000000..69bd3406b4 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/token_account/accounts.rs @@ -0,0 +1,68 @@ +//! Accounts struct for create_token_vault instruction (pinocchio version). + +use borsh::{BorshDeserialize, BorshSerialize}; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +/// Seed constant for token vault PDA +pub const TOKEN_VAULT_SEED: &[u8] = b"vault"; + +/// Minimal params for token vault creation. +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateTokenVaultParams { + pub vault_bump: u8, +} + +/// Accounts struct for creating a PDA token vault. +/// +/// The token vault is created via CPI to light-token program, not system program. +/// parse() only validates the PDA derivation. +pub struct CreateTokenVaultAccounts<'a> { + pub payer: &'a AccountInfo, + pub mint: &'a AccountInfo, + pub vault_owner: &'a AccountInfo, + pub token_vault: &'a AccountInfo, + pub compressible_config: &'a AccountInfo, + pub rent_sponsor: &'a AccountInfo, + pub light_token_program: &'a AccountInfo, + pub system_program: &'a AccountInfo, +} + +impl<'a> CreateTokenVaultAccounts<'a> { + pub const FIXED_LEN: usize = 8; + + pub fn parse(accounts: &'a [AccountInfo]) -> Result { + let payer = &accounts[0]; + let mint = &accounts[1]; + let vault_owner = &accounts[2]; + let token_vault = &accounts[3]; + let compressible_config = &accounts[4]; + let rent_sponsor = &accounts[5]; + let light_token_program = &accounts[6]; + let system_program = &accounts[7]; + + if !payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Validate token_vault PDA + { + let mint_key = mint.key(); + let seeds: &[&[u8]] = &[TOKEN_VAULT_SEED, mint_key]; + let (expected_pda, _bump) = pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if token_vault.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + } + + Ok(Self { + payer, + mint, + vault_owner, + token_vault, + compressible_config, + rent_sponsor, + light_token_program, + system_program, + }) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/token_account/derived.rs b/sdk-tests/pinocchio-manual-test/src/token_account/derived.rs new file mode 100644 index 0000000000..90efc00eae --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/token_account/derived.rs @@ -0,0 +1,118 @@ +//! Derived code - what the macro would generate for token accounts. + +use borsh::{BorshDeserialize, BorshSerialize}; +#[cfg(not(target_os = "solana"))] +use light_account_pinocchio::Pack; +use light_account_pinocchio::{ + light_account_checks::{self}, + CreateTokenAccountCpi, LightFinalize, LightPreInit, LightSdkTypesError, Unpack, +}; +use pinocchio::account_info::AccountInfo; + +use super::accounts::{CreateTokenVaultAccounts, CreateTokenVaultParams, TOKEN_VAULT_SEED}; + +// ============================================================================ +// LightPreInit Implementation - Creates token account at START of instruction +// ============================================================================ + +impl LightPreInit for CreateTokenVaultAccounts<'_> { + fn light_pre_init( + &mut self, + _remaining_accounts: &[AccountInfo], + params: &CreateTokenVaultParams, + ) -> std::result::Result { + let inner = || -> std::result::Result { + // Build PDA seeds: [TOKEN_VAULT_SEED, mint.key(), &[bump]] + let mint_key = *self.mint.key(); + let vault_seeds: &[&[u8]] = + &[TOKEN_VAULT_SEED, mint_key.as_ref(), &[params.vault_bump]]; + + // Create token account via CPI with rent-free mode + // In pinocchio, accounts are already &AccountInfo, no .to_account_info() needed + CreateTokenAccountCpi { + payer: self.payer, + account: self.token_vault, + mint: self.mint, + owner: *self.vault_owner.key(), + } + .rent_free( + self.compressible_config, + self.rent_sponsor, + self.system_program, + &crate::ID, + ) + .invoke_signed(vault_seeds)?; + + // Token accounts don't use CPI context, return false + Ok(false) + }; + inner() + } +} + +// ============================================================================ +// LightFinalize Implementation - No-op for token account only flow +// ============================================================================ + +impl LightFinalize for CreateTokenVaultAccounts<'_> { + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo], + _params: &CreateTokenVaultParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + Ok(()) + } +} + +// ============================================================================ +// Token Vault Seeds (for Pack/Unpack) +// ============================================================================ + +/// Token vault seeds for PDA derivation (client-side). +#[allow(dead_code)] +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct TokenVaultSeeds { + pub mint: [u8; 32], +} + +/// Packed token vault seeds with u8 indices. +#[allow(dead_code)] +#[derive(BorshSerialize, BorshDeserialize, Clone, Debug)] +pub struct PackedTokenVaultSeeds { + pub mint_idx: u8, + pub bump: u8, +} + +// ============================================================================ +// Pack/Unpack Implementations +// ============================================================================ + +#[cfg(not(target_os = "solana"))] +impl Pack for TokenVaultSeeds { + type Packed = PackedTokenVaultSeeds; + fn pack( + &self, + remaining_accounts: &mut light_account_pinocchio::PackedAccounts, + ) -> std::result::Result { + Ok(PackedTokenVaultSeeds { + mint_idx: remaining_accounts.insert_or_get(solana_pubkey::Pubkey::from(self.mint)), + bump: 0, + }) + } +} + +impl Unpack for PackedTokenVaultSeeds { + type Unpacked = TokenVaultSeeds; + + fn unpack( + &self, + remaining_accounts: &[AI], + ) -> std::result::Result { + let mint = remaining_accounts + .get(self.mint_idx as usize) + .ok_or(LightSdkTypesError::NotEnoughAccountKeys)? + .key(); + Ok(TokenVaultSeeds { mint }) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/token_account/mod.rs b/sdk-tests/pinocchio-manual-test/src/token_account/mod.rs new file mode 100644 index 0000000000..28e7ac085e --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/token_account/mod.rs @@ -0,0 +1,6 @@ +//! Token account creation - manual implementation of macro-generated code. + +pub mod accounts; +mod derived; + +pub use accounts::*; diff --git a/sdk-tests/pinocchio-manual-test/src/two_mints/accounts.rs b/sdk-tests/pinocchio-manual-test/src/two_mints/accounts.rs new file mode 100644 index 0000000000..4b4fbe2df8 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/two_mints/accounts.rs @@ -0,0 +1,109 @@ +//! Accounts struct for create_derived_mints instruction (pinocchio version). + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_account_pinocchio::CreateAccountsProof; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +/// Seed constants +pub const MINT_SIGNER_0_SEED: &[u8] = b"mint_signer_0"; +pub const MINT_SIGNER_1_SEED: &[u8] = b"mint_signer_1"; + +/// Minimal params - matches macro pattern. +#[derive(Clone, BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateDerivedMintsParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_0_bump: u8, + pub mint_signer_1_bump: u8, +} + +/// Accounts struct - matches macro pattern with mint signers as PDAs. +pub struct CreateDerivedMintsAccounts<'a> { + pub payer: &'a AccountInfo, + pub authority: &'a AccountInfo, + pub mint_signer_0: &'a AccountInfo, + pub mint_signer_1: &'a AccountInfo, + pub mint_0: &'a AccountInfo, + pub mint_1: &'a AccountInfo, + pub compressible_config: &'a AccountInfo, + pub rent_sponsor: &'a AccountInfo, + pub light_token_program: &'a AccountInfo, + pub cpi_authority: &'a AccountInfo, + pub system_program: &'a AccountInfo, + /// Slice view for mint_signer accounts (for invoke_create_mints) + pub mint_signers_slice: &'a [AccountInfo], + /// Slice view for mint accounts (for invoke_create_mints) + pub mints_slice: &'a [AccountInfo], +} + +impl<'a> CreateDerivedMintsAccounts<'a> { + pub const FIXED_LEN: usize = 11; + + pub fn parse( + accounts: &'a [AccountInfo], + params: &CreateDerivedMintsParams, + ) -> Result { + let payer = &accounts[0]; + let authority = &accounts[1]; + let mint_signer_0 = &accounts[2]; + let mint_signer_1 = &accounts[3]; + let mint_0 = &accounts[4]; + let mint_1 = &accounts[5]; + let compressible_config = &accounts[6]; + let rent_sponsor = &accounts[7]; + let light_token_program = &accounts[8]; + let cpi_authority = &accounts[9]; + let system_program = &accounts[10]; + + // Validate signers + if !payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + if !authority.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Validate mint_signer_0 PDA + { + let authority_key = authority.key(); + let seeds: &[&[u8]] = &[MINT_SIGNER_0_SEED, authority_key]; + let (expected_pda, expected_bump) = + pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if mint_signer_0.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + if expected_bump != params.mint_signer_0_bump { + return Err(ProgramError::InvalidSeeds); + } + } + + // Validate mint_signer_1 PDA + { + let authority_key = authority.key(); + let seeds: &[&[u8]] = &[MINT_SIGNER_1_SEED, authority_key]; + let (expected_pda, expected_bump) = + pinocchio::pubkey::find_program_address(seeds, &crate::ID); + if mint_signer_1.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + if expected_bump != params.mint_signer_1_bump { + return Err(ProgramError::InvalidSeeds); + } + } + + Ok(Self { + payer, + authority, + mint_signer_0, + mint_signer_1, + mint_0, + mint_1, + compressible_config, + rent_sponsor, + light_token_program, + cpi_authority, + system_program, + mint_signers_slice: &accounts[2..4], + mints_slice: &accounts[4..6], + }) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/two_mints/derived.rs b/sdk-tests/pinocchio-manual-test/src/two_mints/derived.rs new file mode 100644 index 0000000000..e06bba669f --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/two_mints/derived.rs @@ -0,0 +1,119 @@ +//! Derived code - what the macro would generate. +//! Contains LightPreInit/LightFinalize trait implementations. + +use light_account_pinocchio::{ + CpiAccounts, CpiAccountsConfig, CreateMints, CreateMintsStaticAccounts, LightFinalize, + LightPreInit, LightSdkTypesError, SingleMintParams, +}; +use pinocchio::account_info::AccountInfo; + +use super::accounts::{ + CreateDerivedMintsAccounts, CreateDerivedMintsParams, MINT_SIGNER_0_SEED, MINT_SIGNER_1_SEED, +}; + +// ============================================================================ +// LightPreInit Implementation - Creates mints at START of instruction +// ============================================================================ + +impl LightPreInit for CreateDerivedMintsAccounts<'_> { + fn light_pre_init( + &mut self, + remaining_accounts: &[AccountInfo], + params: &CreateDerivedMintsParams, + ) -> std::result::Result { + let inner = || -> std::result::Result { + // 1. Build CPI accounts + let system_accounts_offset = + params.create_accounts_proof.system_accounts_offset as usize; + if remaining_accounts.len() < system_accounts_offset { + return Err(LightSdkTypesError::FewerAccountsThanSystemAccounts); + } + let config = CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER); + let cpi_accounts = CpiAccounts::new_with_config( + self.payer, + &remaining_accounts[system_accounts_offset..], + config, + ); + + // Constants + const NUM_LIGHT_MINTS: usize = 2; + const NUM_LIGHT_PDAS: usize = 0; + #[allow(clippy::absurd_extreme_comparisons)] + const WITH_CPI_CONTEXT: bool = NUM_LIGHT_PDAS > 0 && NUM_LIGHT_MINTS > 0; + + // 2. Build mint params + let authority = *self.authority.key(); + let mint_signer_0 = *self.mint_signer_0.key(); + let mint_signer_1 = *self.mint_signer_1.key(); + + let mint_signer_0_seeds: &[&[u8]] = &[ + MINT_SIGNER_0_SEED, + authority.as_ref(), + &[params.mint_signer_0_bump], + ]; + let mint_signer_1_seeds: &[&[u8]] = &[ + MINT_SIGNER_1_SEED, + authority.as_ref(), + &[params.mint_signer_1_bump], + ]; + + let sdk_mints: [SingleMintParams<'_>; NUM_LIGHT_MINTS] = [ + SingleMintParams { + decimals: 6, + mint_authority: authority, + mint_bump: None, + freeze_authority: None, + mint_seed_pubkey: mint_signer_0, + authority_seeds: None, + mint_signer_seeds: Some(mint_signer_0_seeds), + token_metadata: None, + }, + SingleMintParams { + decimals: 9, + mint_authority: authority, + mint_bump: None, + freeze_authority: None, + mint_seed_pubkey: mint_signer_1, + authority_seeds: None, + mint_signer_seeds: Some(mint_signer_1_seeds), + token_metadata: None, + }, + ]; + + // 3. Create mints + CreateMints { + mints: &sdk_mints, + proof_data: ¶ms.create_accounts_proof, + mint_seed_accounts: self.mint_signers_slice, + mint_accounts: self.mints_slice, + static_accounts: CreateMintsStaticAccounts { + fee_payer: self.payer, + compressible_config: self.compressible_config, + rent_sponsor: self.rent_sponsor, + cpi_authority: self.cpi_authority, + }, + cpi_context_offset: NUM_LIGHT_PDAS as u8, + } + .invoke(&cpi_accounts)?; + + Ok(WITH_CPI_CONTEXT) + }; + inner() + } +} + +// ============================================================================ +// LightFinalize Implementation - No-op for mint-only flow +// ============================================================================ + +impl LightFinalize for CreateDerivedMintsAccounts<'_> { + fn light_finalize( + &mut self, + _remaining_accounts: &[AccountInfo], + _params: &CreateDerivedMintsParams, + _has_pre_init: bool, + ) -> std::result::Result<(), LightSdkTypesError> { + // No-op for mint-only flow - create_mints already executed in light_pre_init + Ok(()) + } +} diff --git a/sdk-tests/pinocchio-manual-test/src/two_mints/mod.rs b/sdk-tests/pinocchio-manual-test/src/two_mints/mod.rs new file mode 100644 index 0000000000..2876d485c8 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/src/two_mints/mod.rs @@ -0,0 +1,6 @@ +//! Two mints instruction - creates two compressed mints using derived PDAs. + +pub mod accounts; +mod derived; + +pub use accounts::*; diff --git a/sdk-tests/pinocchio-manual-test/tests/account_loader.rs b/sdk-tests/pinocchio-manual-test/tests/account_loader.rs new file mode 100644 index 0000000000..a72630b068 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/tests/account_loader.rs @@ -0,0 +1,191 @@ +//! Integration test for zero-copy AccountLoader support. +//! +//! Tests the full lifecycle: create -> compress -> decompress +//! for zero-copy accounts (ZeroCopyRecord). + +mod shared; + +use light_account_pinocchio::{CompressionState, IntoVariant, LightDiscriminator}; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, + CreateAccountsProofInput, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Indexer, Rpc}; +use pinocchio_manual_test::{ + account_loader::accounts::CreateZeroCopyParams, ZeroCopyRecord, ZeroCopyRecordSeeds, + ZeroCopyRecordVariant, +}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_sdk::instruction::{AccountMeta, Instruction}; +use solana_signer::Signer; + +/// Test the full lifecycle for zero-copy accounts: create -> compress -> decompress. +#[tokio::test] +async fn test_zero_copy_create_compress_decompress() { + let program_id = Pubkey::new_from_array(pinocchio_manual_test::ID); + let (mut rpc, payer, config_pda) = shared::setup_test_env().await; + + let owner = Keypair::new().pubkey(); + let value: u64 = 12345; + let name = "my_record".to_string(); + + // Derive PDA for zero-copy record + let (record_pda, _) = Pubkey::find_program_address( + &[b"zero_copy", owner.as_ref(), name.as_bytes()], + &program_id, + ); + + // Get proof for the PDA + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::pda(record_pda)], + ) + .await + .unwrap(); + + let params = CreateZeroCopyParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner: owner.to_bytes(), + value, + name: name.clone(), + }; + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new(record_pda, false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let ix = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: [ + pinocchio_manual_test::discriminators::CREATE_ZERO_COPY.as_slice(), + &borsh::to_vec(¶ms).unwrap(), + ] + .concat(), + }; + + rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) + .await + .expect("CreateZeroCopy should succeed"); + + // PHASE 1: Verify account exists on-chain + assert!( + rpc.get_account(record_pda).await.unwrap().is_some(), + "Account should exist on-chain after creation" + ); + + // PHASE 2: Warp time to trigger forester auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + + // Verify account is closed on-chain (compressed by forester) + let acc = rpc.get_account(record_pda).await.unwrap(); + assert!( + acc.is_none() || acc.unwrap().lamports == 0, + "Account should be closed after compression" + ); + + // PHASE 3: Verify compressed account exists + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + let compressed_address = light_compressed_account::address::derive_address( + &record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_acc = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + assert_eq!( + compressed_acc.address.unwrap(), + compressed_address, + "Compressed account address should match" + ); + assert!( + !compressed_acc.data.as_ref().unwrap().data.is_empty(), + "Compressed account should have data" + ); + + // PHASE 4: Decompress account + let account_interface = rpc + .get_account_interface(&record_pda, &program_id) + .await + .expect("failed to get account interface"); + assert!( + account_interface.is_cold(), + "Account should be cold (compressed)" + ); + + // Build variant using IntoVariant - verify seeds match the compressed data + let variant = ZeroCopyRecordSeeds { + owner: owner.to_bytes(), + name: name.clone(), + } + .into_variant(&account_interface.account.data[8..]) + .expect("Seed verification failed"); + + // Verify the data from the compressed account + assert_eq!(variant.data.value, value, "Compressed value should match"); + assert_eq!( + Pubkey::new_from_array(variant.data.owner), + owner, + "Compressed owner should match" + ); + + // Build PdaSpec and create decompress instructions + let spec = PdaSpec::new(account_interface.clone(), variant, program_id); + let specs: Vec> = vec![AccountSpec::Pda(spec)]; + + let decompress_instructions = + create_load_instructions(&specs, payer.pubkey(), config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&decompress_instructions, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 5: Verify account is back on-chain with correct data + let record_account = rpc + .get_account(record_pda) + .await + .unwrap() + .expect("Account should exist after decompression"); + + // Verify discriminator is set correctly (first 8 bytes) + let discriminator = &record_account.data[..8]; + assert_eq!( + discriminator, + ZeroCopyRecord::LIGHT_DISCRIMINATOR, + "Discriminator should match ZeroCopyRecord::LIGHT_DISCRIMINATOR after decompression" + ); + + // Verify data is correct (zero-copy uses bytemuck) + let record_bytes = &record_account.data[8..8 + core::mem::size_of::()]; + let record: &ZeroCopyRecord = bytemuck::from_bytes(record_bytes); + + assert_eq!( + Pubkey::new_from_array(record.owner), + owner, + "Record owner should match after decompression" + ); + assert_eq!( + record.value, value, + "Record value should match after decompression" + ); + + // state should be Decompressed after decompression + assert_eq!( + record.compression_info.state, + CompressionState::Decompressed, + "state should be Decompressed after decompression" + ); +} diff --git a/sdk-tests/pinocchio-manual-test/tests/all.rs b/sdk-tests/pinocchio-manual-test/tests/all.rs new file mode 100644 index 0000000000..c3b1c2d748 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/tests/all.rs @@ -0,0 +1,220 @@ +//! Test create_all instruction - all account types in a single instruction. +//! +//! Creates: +//! - Borsh PDA (MinimalRecord) +//! - ZeroCopy PDA (ZeroCopyRecord) +//! - Compressed Mint +//! - Token Vault +//! - Associated Token Account (ATA) + +mod shared; + +use borsh::BorshDeserialize; +use light_client::interface::{get_create_accounts_proof, CreateAccountsProofInput}; +use light_program_test::Rpc; +use light_token::instruction::{ + config_pda, derive_associated_token_account, find_mint_address, rent_sponsor_pda, + LIGHT_TOKEN_PROGRAM_ID, +}; +use light_token_interface::state::{ + AccountState, BaseMint, Mint, MintMetadata, Token, ACCOUNT_TYPE_MINT, +}; +use pinocchio_manual_test::{ + all::accounts::{ + CreateAllParams, ALL_BORSH_SEED, ALL_MINT_SIGNER_SEED, ALL_TOKEN_VAULT_SEED, + ALL_ZERO_COPY_SEED, + }, + MinimalRecord, ZeroCopyRecord, +}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +/// Test creating all account types in a single instruction. +#[tokio::test] +async fn test_create_all() { + let (mut rpc, payer, config_pda_addr) = shared::setup_test_env().await; + + let program_id = Pubkey::new_from_array(pinocchio_manual_test::ID); + let authority = Keypair::new(); + let owner = Keypair::new().pubkey(); + let value: u64 = 42; + + // ========== Derive all addresses ========== + + // PDAs (using ALL module-specific seeds) + let (borsh_record_pda, _) = + Pubkey::find_program_address(&[ALL_BORSH_SEED, owner.as_ref()], &program_id); + let (zero_copy_record_pda, _) = + Pubkey::find_program_address(&[ALL_ZERO_COPY_SEED, owner.as_ref()], &program_id); + + // Mint signer and mint + let (mint_signer, mint_signer_bump) = Pubkey::find_program_address( + &[ALL_MINT_SIGNER_SEED, authority.pubkey().as_ref()], + &program_id, + ); + let (mint, _mint_bump) = find_mint_address(&mint_signer); + + // Token vault + let (token_vault, token_vault_bump) = + Pubkey::find_program_address(&[ALL_TOKEN_VAULT_SEED, mint.as_ref()], &program_id); + let vault_owner = Keypair::new(); + + // ATA + let ata_owner = Keypair::new(); + let (user_ata, _) = derive_associated_token_account(&ata_owner.pubkey(), &mint); + + // ========== Get proof for 2 PDAs + 1 Mint ========== + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::pda(borsh_record_pda), + CreateAccountsProofInput::pda(zero_copy_record_pda), + CreateAccountsProofInput::mint(mint_signer), + ], + ) + .await + .unwrap(); + + // ========== Build and send instruction ========== + let params = CreateAllParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump, + token_vault_bump, + owner: owner.to_bytes(), + value, + }; + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(authority.pubkey(), true), + AccountMeta::new_readonly(config_pda_addr, false), + AccountMeta::new(borsh_record_pda, false), + AccountMeta::new(zero_copy_record_pda, false), + AccountMeta::new_readonly(mint_signer, false), + AccountMeta::new(mint, false), + AccountMeta::new(token_vault, false), + AccountMeta::new_readonly(vault_owner.pubkey(), false), + AccountMeta::new_readonly(ata_owner.pubkey(), false), + AccountMeta::new(user_ata, false), + AccountMeta::new_readonly(config_pda(), false), + AccountMeta::new(rent_sponsor_pda(), false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly( + Pubkey::new_from_array(light_token_types::CPI_AUTHORITY_PDA), + false, + ), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let ix = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: [ + pinocchio_manual_test::discriminators::CREATE_ALL.as_slice(), + &borsh::to_vec(¶ms).unwrap(), + ] + .concat(), + }; + + rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateAll should succeed"); + + // ========== Verify all 5 accounts exist with correct data ========== + + // 1. Verify Borsh PDA + let borsh_account = rpc + .get_account(borsh_record_pda) + .await + .unwrap() + .expect("Borsh PDA should exist"); + + let borsh_record = + MinimalRecord::deserialize(&mut &borsh_account.data[8..]).expect("Should deserialize"); + assert_eq!( + borsh_record.owner, + owner.to_bytes(), + "Borsh PDA owner should match" + ); + + // 2. Verify ZeroCopy PDA + let zero_copy_account = rpc + .get_account(zero_copy_record_pda) + .await + .unwrap() + .expect("ZeroCopy PDA should exist"); + + let record_bytes = &zero_copy_account.data[8..8 + core::mem::size_of::()]; + let record: &ZeroCopyRecord = bytemuck::from_bytes(record_bytes); + assert_eq!( + Pubkey::new_from_array(record.owner), + owner, + "ZeroCopy PDA owner should match" + ); + assert_eq!(record.value, value, "ZeroCopy PDA value should match"); + + // 3. Verify Mint + let mint_account = rpc + .get_account(mint) + .await + .unwrap() + .expect("Mint should exist"); + + let mint_data = Mint::deserialize(&mut &mint_account.data[..]).expect("Should deserialize"); + let compression = mint_data.compression; + + let expected_mint = Mint { + base: BaseMint { + mint_authority: Some(authority.pubkey().to_bytes().into()), + supply: 0, + decimals: 6, + is_initialized: true, + freeze_authority: None, + }, + metadata: MintMetadata { + version: 3, + mint_decompressed: true, + mint: mint.to_bytes().into(), + mint_signer: mint_signer.to_bytes(), + bump: _mint_bump, + }, + reserved: [0u8; 16], + account_type: ACCOUNT_TYPE_MINT, + compression, + extensions: None, + }; + + assert_eq!(mint_data, expected_mint, "Mint should match expected"); + + // 4. Verify Token Vault + let vault_account = rpc + .get_account(token_vault) + .await + .unwrap() + .expect("Token vault should exist"); + + let token = + Token::deserialize(&mut &vault_account.data[..]).expect("Should deserialize as Token"); + assert_eq!(token.mint.to_bytes(), mint.to_bytes()); + assert_eq!(token.owner.to_bytes(), vault_owner.pubkey().to_bytes()); + assert_eq!(token.amount, 0); + assert_eq!(token.state, AccountState::Initialized); + + // 5. Verify ATA + let ata_account = rpc + .get_account(user_ata) + .await + .unwrap() + .expect("ATA should exist"); + + let ata_token = + Token::deserialize(&mut &ata_account.data[..]).expect("Should deserialize as Token"); + assert_eq!(ata_token.mint.to_bytes(), mint.to_bytes()); + assert_eq!(ata_token.owner.to_bytes(), ata_owner.pubkey().to_bytes()); + assert_eq!(ata_token.amount, 0); + assert_eq!(ata_token.state, AccountState::Initialized); +} diff --git a/sdk-tests/pinocchio-manual-test/tests/ata.rs b/sdk-tests/pinocchio-manual-test/tests/ata.rs new file mode 100644 index 0000000000..eda13b3b58 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/tests/ata.rs @@ -0,0 +1,130 @@ +//! Test ATA pattern - Associated Token Account with rent-free CPI. + +mod shared; + +use borsh::BorshDeserialize; +use light_program_test::Rpc; +use light_token::instruction::{ + config_pda, derive_associated_token_account, rent_sponsor_pda, LIGHT_TOKEN_PROGRAM_ID, +}; +use light_token_interface::state::{AccountState, Token}; +use pinocchio_manual_test::CreateAtaParams; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +/// Test creating an ATA using CreateTokenAtaCpi. +#[tokio::test] +async fn test_create_ata() { + let (mut rpc, payer, _) = shared::setup_test_env().await; + + // Create a mint to use for the ATA + let mint = shared::create_test_mint(&mut rpc, &payer).await; + + // ATA owner - typically a user wallet + let ata_owner = Keypair::new(); + + // Derive ATA address using light-token's standard derivation + let (user_ata, _) = derive_associated_token_account(&ata_owner.pubkey(), &mint); + + let params = CreateAtaParams::default(); + + let program_id = Pubkey::new_from_array(pinocchio_manual_test::ID); + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(mint, false), + AccountMeta::new_readonly(ata_owner.pubkey(), false), + AccountMeta::new(user_ata, false), + AccountMeta::new_readonly(config_pda(), false), + AccountMeta::new(rent_sponsor_pda(), false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let data = [ + pinocchio_manual_test::discriminators::CREATE_ATA.as_slice(), + &borsh::to_vec(¶ms).unwrap(), + ] + .concat(); + + let ix = Instruction { + program_id, + accounts, + data, + }; + + rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) + .await + .expect("CreateAta should succeed"); + + // Verify ATA exists and has correct state + let ata_account = rpc + .get_account(user_ata) + .await + .unwrap() + .expect("ATA should exist"); + + let token = + Token::deserialize(&mut &ata_account.data[..]).expect("Should deserialize as Token"); + + assert_eq!(token.mint.to_bytes(), mint.to_bytes()); + assert_eq!(token.owner.to_bytes(), ata_owner.pubkey().to_bytes()); + assert_eq!(token.amount, 0); + assert_eq!(token.state, AccountState::Initialized); +} + +/// Test idempotent ATA creation - should not fail if ATA already exists. +#[tokio::test] +async fn test_create_ata_idempotent() { + let (mut rpc, payer, _) = shared::setup_test_env().await; + + let mint = shared::create_test_mint(&mut rpc, &payer).await; + let ata_owner = Keypair::new(); + let (user_ata, _) = derive_associated_token_account(&ata_owner.pubkey(), &mint); + + let params = CreateAtaParams::default(); + + let program_id = Pubkey::new_from_array(pinocchio_manual_test::ID); + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(mint, false), + AccountMeta::new_readonly(ata_owner.pubkey(), false), + AccountMeta::new(user_ata, false), + AccountMeta::new_readonly(config_pda(), false), + AccountMeta::new(rent_sponsor_pda(), false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let data = [ + pinocchio_manual_test::discriminators::CREATE_ATA.as_slice(), + &borsh::to_vec(¶ms).unwrap(), + ] + .concat(); + + let ix = Instruction { + program_id, + accounts: accounts.clone(), + data: data.clone(), + }; + + // First creation + rpc.create_and_send_transaction(std::slice::from_ref(&ix), &payer.pubkey(), &[&payer]) + .await + .expect("First CreateAta should succeed"); + + // Second creation (idempotent) - should NOT fail + let ix2 = Instruction { + program_id, + accounts, + data, + }; + + rpc.create_and_send_transaction(&[ix2], &payer.pubkey(), &[&payer]) + .await + .expect("Second CreateAta should succeed (idempotent)"); +} diff --git a/sdk-tests/pinocchio-manual-test/tests/shared.rs b/sdk-tests/pinocchio-manual-test/tests/shared.rs new file mode 100644 index 0000000000..6289b74524 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/tests/shared.rs @@ -0,0 +1,123 @@ +//! Shared test helpers for manual-test-pinocchio integration tests. + +use light_account_pinocchio::derive_rent_sponsor_pda; +use light_client::interface::{ + get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, +}; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest}, + ProgramTestConfig, Rpc, +}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Setup test environment with Light Protocol and compression config. +/// Returns (rpc, payer, config_pda). +pub async fn setup_test_env() -> (LightProgramTest, Keypair, Pubkey) { + let program_id = Pubkey::new_from_array(pinocchio_manual_test::ID); + let mut config = + ProgramTestConfig::new_v2(true, Some(vec![("pinocchio_manual_test", program_id)])); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + // Derive rent sponsor PDA for this program (pinocchio version takes &[u8; 32]) + let (rent_sponsor_bytes, _) = derive_rent_sponsor_pda(&program_id.to_bytes()); + let rent_sponsor = Pubkey::new_from_array(rent_sponsor_bytes); + + 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"); + + (rpc, payer, config_pda) +} + +/// Create a test mint using the two_mints instruction and return the mint pubkey. +#[allow(dead_code)] +pub async fn create_test_mint(rpc: &mut LightProgramTest, payer: &Keypair) -> Pubkey { + use pinocchio_manual_test::two_mints::accounts::{ + CreateDerivedMintsParams, MINT_SIGNER_0_SEED, MINT_SIGNER_1_SEED, + }; + use solana_sdk::instruction::{AccountMeta, Instruction}; + + let program_id = Pubkey::new_from_array(pinocchio_manual_test::ID); + let authority = Keypair::new(); + + // Derive mint signer PDAs + let (mint_signer_0, mint_signer_0_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_0_SEED, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_signer_1, mint_signer_1_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_1_SEED, authority.pubkey().as_ref()], + &program_id, + ); + + // Derive mint PDAs + let (mint_0, _) = light_token::instruction::find_mint_address(&mint_signer_0); + let (mint_1, _) = light_token::instruction::find_mint_address(&mint_signer_1); + + // Get proof for the mints + let proof_result = get_create_accounts_proof( + rpc, + &program_id, + vec![ + CreateAccountsProofInput::mint(mint_signer_0), + CreateAccountsProofInput::mint(mint_signer_1), + ], + ) + .await + .unwrap(); + + let params = CreateDerivedMintsParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_0_bump, + mint_signer_1_bump, + }; + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(authority.pubkey(), true), + AccountMeta::new_readonly(mint_signer_0, false), + AccountMeta::new_readonly(mint_signer_1, false), + AccountMeta::new(mint_0, false), + AccountMeta::new(mint_1, false), + AccountMeta::new_readonly(light_token::instruction::config_pda(), false), + AccountMeta::new(light_token::instruction::rent_sponsor_pda(), false), + AccountMeta::new_readonly(light_token::instruction::LIGHT_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly( + Pubkey::new_from_array(light_token_types::CPI_AUTHORITY_PDA), + false, + ), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let ix = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: [ + pinocchio_manual_test::discriminators::CREATE_DERIVED_MINTS.as_slice(), + &borsh::to_vec(¶ms).unwrap(), + ] + .concat(), + }; + + rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[payer, &authority]) + .await + .expect("Create mint should succeed"); + + mint_0 // Return first mint +} diff --git a/sdk-tests/pinocchio-manual-test/tests/test.rs b/sdk-tests/pinocchio-manual-test/tests/test.rs new file mode 100644 index 0000000000..e652b91fdc --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/tests/test.rs @@ -0,0 +1,164 @@ +//! Integration test for manual Light Protocol implementation. +//! +//! Tests the full lifecycle: create -> compress -> decompress + +mod shared; + +use light_account_pinocchio::{CompressionState, IntoVariant}; +use light_client::interface::{ + create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, + CreateAccountsProofInput, PdaSpec, +}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_program_test::{program_test::TestRpc, Indexer, Rpc}; +use pinocchio_manual_test::{ + pda::accounts::CreatePdaParams, MinimalRecord, MinimalRecordSeeds, MinimalRecordVariant, +}; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_sdk::instruction::{AccountMeta, Instruction}; +use solana_signer::Signer; + +/// Test the full lifecycle: create -> compress -> decompress. +#[tokio::test] +async fn test_create_compress_decompress() { + let program_id = Pubkey::new_from_array(pinocchio_manual_test::ID); + let (mut rpc, payer, config_pda) = shared::setup_test_env().await; + + let owner = Keypair::new().pubkey(); + let nonce: u64 = 12345; + + // Derive PDA for record + let (record_pda, _) = Pubkey::find_program_address( + &[b"minimal_record", owner.as_ref(), &nonce.to_le_bytes()], + &program_id, + ); + + // Get proof for the PDA + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::pda(record_pda)], + ) + .await + .unwrap(); + + let params = CreatePdaParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner: owner.to_bytes(), + nonce, + }; + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new(record_pda, false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let ix = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: [ + pinocchio_manual_test::discriminators::CREATE_PDA.as_slice(), + &borsh::to_vec(¶ms).unwrap(), + ] + .concat(), + }; + + rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) + .await + .expect("CreatePda should succeed"); + + // PHASE 1: Verify account exists on-chain + assert!( + rpc.get_account(record_pda).await.unwrap().is_some(), + "Account should exist on-chain after creation" + ); + + // PHASE 2: Warp time to trigger forester auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + + // Verify account is closed on-chain (compressed by forester) + let acc = rpc.get_account(record_pda).await.unwrap(); + assert!( + acc.is_none() || acc.unwrap().lamports == 0, + "Account should be closed after compression" + ); + + // PHASE 3: Verify compressed account exists + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + let compressed_address = light_compressed_account::address::derive_address( + &record_pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ); + + let compressed_acc = rpc + .get_compressed_account(compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + assert_eq!( + compressed_acc.address.unwrap(), + compressed_address, + "Compressed account address should match" + ); + assert!( + !compressed_acc.data.as_ref().unwrap().data.is_empty(), + "Compressed account should have data" + ); + + // PHASE 4: Decompress account + let account_interface = rpc + .get_account_interface(&record_pda, &program_id) + .await + .expect("failed to get account interface"); + assert!( + account_interface.is_cold(), + "Account should be cold (compressed)" + ); + + // Build variant using IntoVariant - verify seeds match the compressed data + let variant = MinimalRecordSeeds { + owner: owner.to_bytes(), + nonce, + } + .into_variant(&account_interface.account.data[8..]) + .expect("Seed verification failed"); + + // Build PdaSpec and create decompress instructions + let spec = PdaSpec::new(account_interface.clone(), variant, program_id); + let specs: Vec> = vec![AccountSpec::Pda(spec)]; + + let decompress_instructions = + create_load_instructions(&specs, payer.pubkey(), config_pda, &rpc) + .await + .expect("create_load_instructions should succeed"); + + rpc.create_and_send_transaction(&decompress_instructions, &payer.pubkey(), &[&payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 5: Verify account is back on-chain with correct data + let record_account = rpc + .get_account(record_pda) + .await + .unwrap() + .expect("Account should exist after decompression"); + + // Verify data is correct + let record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &record_account.data[8..]) + .expect("Failed to deserialize MinimalRecord"); + + assert_eq!(record.owner, owner.to_bytes(), "Record owner should match"); + + // state should be Decompressed after decompression + assert_eq!( + record.compression_info.state, + CompressionState::Decompressed, + "state should be Decompressed after decompression" + ); +} diff --git a/sdk-tests/pinocchio-manual-test/tests/token_account.rs b/sdk-tests/pinocchio-manual-test/tests/token_account.rs new file mode 100644 index 0000000000..cef70efd6b --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/tests/token_account.rs @@ -0,0 +1,74 @@ +//! Test token vault pattern - PDA token account with rent-free CPI. + +mod shared; + +use borsh::BorshDeserialize; +use light_program_test::Rpc; +use light_token::instruction::{config_pda, rent_sponsor_pda, LIGHT_TOKEN_PROGRAM_ID}; +use light_token_interface::state::{AccountState, Token}; +use pinocchio_manual_test::{CreateTokenVaultParams, TOKEN_VAULT_SEED}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +/// Test creating a PDA token vault using CreateTokenAccountCpi. +#[tokio::test] +async fn test_create_token_vault() { + let (mut rpc, payer, _) = shared::setup_test_env().await; + + // Create a mint to use for the token vault + let mint = shared::create_test_mint(&mut rpc, &payer).await; + + // Vault owner - can be any pubkey (e.g., a PDA authority) + let vault_owner = Keypair::new(); + + let program_id = Pubkey::new_from_array(pinocchio_manual_test::ID); + + // Derive token vault PDA + let (token_vault, vault_bump) = + Pubkey::find_program_address(&[TOKEN_VAULT_SEED, mint.as_ref()], &program_id); + + let params = CreateTokenVaultParams { vault_bump }; + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(mint, false), + AccountMeta::new_readonly(vault_owner.pubkey(), false), + AccountMeta::new(token_vault, false), + AccountMeta::new_readonly(config_pda(), false), + AccountMeta::new(rent_sponsor_pda(), false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let ix = Instruction { + program_id, + accounts, + data: [ + pinocchio_manual_test::discriminators::CREATE_TOKEN_VAULT.as_slice(), + &borsh::to_vec(¶ms).unwrap(), + ] + .concat(), + }; + + rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &[&payer]) + .await + .expect("CreateTokenVault should succeed"); + + // Verify token account exists and has correct state + let vault_account = rpc + .get_account(token_vault) + .await + .unwrap() + .expect("Token vault should exist"); + + let token = + Token::deserialize(&mut &vault_account.data[..]).expect("Should deserialize as Token"); + + assert_eq!(token.mint.to_bytes(), mint.to_bytes()); + assert_eq!(token.owner.to_bytes(), vault_owner.pubkey().to_bytes()); + assert_eq!(token.amount, 0); + assert_eq!(token.state, AccountState::Initialized); +} diff --git a/sdk-tests/pinocchio-manual-test/tests/two_mints.rs b/sdk-tests/pinocchio-manual-test/tests/two_mints.rs new file mode 100644 index 0000000000..ffb9831932 --- /dev/null +++ b/sdk-tests/pinocchio-manual-test/tests/two_mints.rs @@ -0,0 +1,161 @@ +//! Test derived mint pattern - minimal params, program derives everything. + +mod shared; + +use borsh::BorshDeserialize; +use light_client::interface::{get_create_accounts_proof, CreateAccountsProofInput}; +use light_program_test::Rpc; +use light_token::instruction::{ + config_pda, find_mint_address, rent_sponsor_pda, LIGHT_TOKEN_PROGRAM_ID, +}; +use light_token_interface::state::{BaseMint, Mint, MintMetadata, ACCOUNT_TYPE_MINT}; +use pinocchio_manual_test::two_mints::accounts::{ + CreateDerivedMintsParams, MINT_SIGNER_0_SEED, MINT_SIGNER_1_SEED, +}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::{Keypair, Signer}, +}; + +/// Test creating two compressed mints using derived PDA mint signers. +#[tokio::test] +async fn test_create_derived_mints() { + let (mut rpc, payer, _) = shared::setup_test_env().await; + + let program_id = Pubkey::new_from_array(pinocchio_manual_test::ID); + let authority = Keypair::new(); + + // Derive mint signer PDAs from authority (like macro would) + let (mint_signer_0, mint_signer_0_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_0_SEED, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_signer_1, mint_signer_1_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_1_SEED, authority.pubkey().as_ref()], + &program_id, + ); + + // Derive mint PDAs from mint signers (light-token derives these) + let (mint_0, mint_0_bump) = find_mint_address(&mint_signer_0); + let (mint_1, mint_1_bump) = find_mint_address(&mint_signer_1); + + // Get proof for the mints + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::mint(mint_signer_0), + CreateAccountsProofInput::mint(mint_signer_1), + ], + ) + .await + .unwrap(); + + // Minimal params - only proof + bumps + let params = CreateDerivedMintsParams { + create_accounts_proof: proof_result.create_accounts_proof.clone(), + mint_signer_0_bump, + mint_signer_1_bump, + }; + + let accounts = vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(authority.pubkey(), true), + AccountMeta::new_readonly(mint_signer_0, false), + AccountMeta::new_readonly(mint_signer_1, false), + AccountMeta::new(mint_0, false), + AccountMeta::new(mint_1, false), + AccountMeta::new_readonly(config_pda(), false), + AccountMeta::new(rent_sponsor_pda(), false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly( + Pubkey::new_from_array(light_token_types::CPI_AUTHORITY_PDA), + false, + ), + AccountMeta::new_readonly(solana_sdk::system_program::ID, false), + ]; + + let ix = Instruction { + program_id, + accounts: [accounts, proof_result.remaining_accounts].concat(), + data: [ + pinocchio_manual_test::discriminators::CREATE_DERIVED_MINTS.as_slice(), + &borsh::to_vec(¶ms).unwrap(), + ] + .concat(), + }; + + // Sign with payer and authority + let signers: Vec<&Keypair> = vec![&payer, &authority]; + + rpc.create_and_send_transaction(&[ix], &payer.pubkey(), &signers) + .await + .expect("CreateDerivedMints should succeed"); + + // Verify mints exist on-chain + let mint_0_account = rpc + .get_account(mint_0) + .await + .unwrap() + .expect("Mint 0 should exist"); + let mint_1_account = rpc + .get_account(mint_1) + .await + .unwrap() + .expect("Mint 1 should exist"); + + // Deserialize and verify mint 0 + let mint_0_data = Mint::deserialize(&mut &mint_0_account.data[..]).unwrap(); + let compression_0 = mint_0_data.compression; + + let expected_mint_0 = Mint { + base: BaseMint { + mint_authority: Some(authority.pubkey().to_bytes().into()), + supply: 0, + decimals: 6, // mint::decimals = 6 + is_initialized: true, + freeze_authority: None, + }, + metadata: MintMetadata { + version: 3, + mint_decompressed: true, + mint: mint_0.to_bytes().into(), + mint_signer: mint_signer_0.to_bytes(), + bump: mint_0_bump, + }, + reserved: [0u8; 16], + account_type: ACCOUNT_TYPE_MINT, + compression: compression_0, + extensions: None, + }; + + assert_eq!(mint_0_data, expected_mint_0, "Mint 0 should match expected"); + + // Deserialize and verify mint 1 + let mint_1_data = Mint::deserialize(&mut &mint_1_account.data[..]).unwrap(); + let compression_1 = mint_1_data.compression; + + let expected_mint_1 = Mint { + base: BaseMint { + mint_authority: Some(authority.pubkey().to_bytes().into()), + supply: 0, + decimals: 9, // mint::decimals = 9 + is_initialized: true, + freeze_authority: None, + }, + metadata: MintMetadata { + version: 3, + mint_decompressed: true, + mint: mint_1.to_bytes().into(), + mint_signer: mint_signer_1.to_bytes(), + bump: mint_1_bump, + }, + reserved: [0u8; 16], + account_type: ACCOUNT_TYPE_MINT, + compression: compression_1, + extensions: None, + }; + + assert_eq!(mint_1_data, expected_mint_1, "Mint 1 should match expected"); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/Cargo.toml b/sdk-tests/sdk-light-token-pinocchio/Cargo.toml new file mode 100644 index 0000000000..1da8cbaef5 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "sdk-light-token-pinocchio-test" +version = "0.1.0" +description = "Pinocchio-based Solana program demonstrating compressed token operations" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "sdk_light_token_pinocchio_test" +doctest = false + +[features] +no-entrypoint = [] +test-sbf = [] +test-helpers = ["dep:light-program-test", "dep:solana-sdk"] +default = [] + +[dependencies] +# Light Protocol SDK dependencies (workspace-based) +light-token-pinocchio = { workspace = true } +light-macros = { workspace = true } + +# Solana dependencies +pinocchio = { workspace = true } + +# Serialization +borsh = "0.10.4" + +# Optional test dependencies +light-program-test = { workspace = true, optional = true } +solana-sdk = { version = "2.2", optional = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +light-client = { workspace = true } +light-compressible = { workspace = true } +light-compressed-account = { workspace = true } +light-token = { workspace = true } +light-token-client = { workspace = true } +light-token-interface = { workspace = true } +light-token-types = { workspace = true } +light-sdk = { workspace = true, features = ["v2"] } +light-test-utils = { workspace = true, features = ["devenv"] } +tokio = { version = "1.36.0", features = ["full"] } +solana-sdk = "2.2" +spl-token-2022 = { workspace = true } +spl-pod = { workspace = true } +anchor-spl = "0.31.1" + +[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-light-token-pinocchio/README.md b/sdk-tests/sdk-light-token-pinocchio/README.md new file mode 100644 index 0000000000..c8534c461d --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/README.md @@ -0,0 +1,171 @@ +# Native CToken Examples + +This program demonstrates how to use compressed tokens (ctokens) from Light Protocol in a native Solana program (no Anchor framework). + +## Overview + +The program showcases **8 different instructions** that cover the core compressed token operations: + +1. **create_cmint** - Create a compressed mint +2. **mint_to_ctoken** - Mint tokens to compressed accounts +3. **create_token_account_invoke** - Create compressible token account (regular authority) +4. **create_token_account_invoke_signed** - Create compressible token account with PDA ownership +5. **create_ata_invoke** - Create compressible associated token account (regular owner) +6. **create_ata_invoke_signed** - Create compressible ATA with PDA ownership +7. **transfer_interface_invoke** - Transfer compressed tokens (regular authority) +8. **transfer_interface_invoke_signed** - Transfer from PDA-owned account + +## Implementation Pattern: Builder Pattern from `ctoken` Module + +This implementation uses the **builder pattern** from the `light-token::ctoken` module. This pattern provides a clean, ergonomic API for CPI operations. + +### Why Use the Builder Pattern? + +The builder pattern offers several advantages: + +- **Type Safety**: Compile-time guarantees for account structures +- **Cleaner Code**: No manual instruction building or account ordering +- **Automatic CPI Handling**: The `invoke()` and `invoke_signed()` methods handle all CPI details +- **Self-Documenting**: Account names make it clear what each field represents + +### Example: Transfer with Builder Pattern + +```rust +// Build the account infos struct +let transfer_accounts = TransferCTokenCpi { + source: accounts[0].clone(), + destination: accounts[1].clone(), + amount: data.amount, + authority: accounts[2].clone(), +}; + +// Invoke the transfer - the builder handles instruction creation and CPI +transfer_accounts.invoke()?; +``` + +### Example: Transfer with PDA Signing (invoke_signed) + +```rust +// Derive PDA +let (pda, bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + +// Build the account infos struct +let transfer_accounts = TransferCTokenCpi { + source: accounts[0].clone(), + destination: accounts[1].clone(), + amount: data.amount, + authority: accounts[2].clone(), +}; + +// Invoke with PDA signing +let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; +transfer_accounts.invoke_signed(&[signer_seeds])?; +``` + +## Current Implementation Status + +### ✅ Fully Implemented (8/8 Instructions) + +All instructions use the **builder pattern** from `light-token::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 `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()` +- **transfer_interface_invoke** (Instruction 6): Transfer using `TransferCTokenCpi::invoke()` +- **transfer_interface_invoke_signed** (Instruction 7): Transfer with PDA signing using `invoke_signed()` + +All instructions compile successfully and demonstrate the clean builder pattern API with constructor usage. + +## Project Structure + +``` +ctoken/native/ +├── Cargo.toml # Path dependencies to light-protocol2/sdk-libs +├── Xargo.toml # Solana BPF build configuration +├── src/ +│ └── lib.rs # Program entrypoint and instruction handlers +└── README.md # This file +``` + +## Dependencies + +All dependencies use **path references** to `/Users/ananas/dev/light-protocol2/sdk-libs/`: + +- `light-token` → Main SDK with ctoken builder pattern +- `light-token-types` → Type definitions +- `light-sdk` → Core SDK +- `light-sdk-types` → Common types +- `light-program-test` → Testing framework (dev dependency) +- `light-client` → RPC client (dev dependency) + +## Building + +```bash +# Check compilation +cargo check + +# Build for BPF +cargo build-sbf + +# Run unit tests +cargo test + +# Run integration tests +cargo test-sbf +``` + +## Key Concepts + +### 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 + +### 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 +- Automated market makers + +### Builder Pattern Benefits + +The `Cpi` structs from the `ctoken` module provide: + +1. **invoke()** - For regular CPI calls where the program acts as authority +2. **invoke_signed()** - For PDA-signed CPI calls +3. **instruction()** - For building instructions without immediate invocation + +## Next Steps + +To complete this example program: + +1. Wait for or implement `AccountInfos` builders for: + - Create compressed mint + - Mint to compressed + - Create token account + - Create associated token account + +2. Add comprehensive integration tests using `light-program-test` + +3. Create example client code demonstrating how to call each instruction + +## References + +- [Light Protocol Documentation](https://www.lightprotocol.com/developers) +- [Compressed Token SDK Source](/Users/ananas/dev/light-protocol2/sdk-libs/compressed-token-sdk) +- [CToken Module](/Users/ananas/dev/light-protocol2/sdk-libs/compressed-token-sdk/src/ctoken) diff --git a/sdk-tests/sdk-light-token-pinocchio/Xargo.toml b/sdk-tests/sdk-light-token-pinocchio/Xargo.toml new file mode 100644 index 0000000000..475fb71ed1 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/sdk-tests/sdk-light-token-pinocchio/src/approve.rs b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs new file mode 100644 index 0000000000..3aacf4d3ed --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/approve.rs @@ -0,0 +1,81 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_token_pinocchio::instruction::ApproveCpi; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::{ID, TOKEN_ACCOUNT_SEED}; + +/// Instruction data for approve operations +#[derive(BorshSerialize, BorshDeserialize)] +pub struct ApproveData { + pub amount: u64, +} + +/// Handler for approving a delegate for a Light Token account (invoke) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: delegate +/// - accounts[2]: owner (signer) +/// - accounts[3]: system_program +/// - accounts[4]: light_token_program +pub fn process_approve_invoke( + accounts: &[AccountInfo], + data: ApproveData, +) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + ApproveCpi { + token_account: &accounts[0], + delegate: &accounts[1], + owner: &accounts[2], + system_program: &accounts[3], + amount: data.amount, + } + .invoke()?; + + Ok(()) +} + +/// Handler for approving a delegate for a PDA-owned Light Token account (invoke_signed) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: delegate +/// - accounts[2]: PDA owner (program signs) +/// - accounts[3]: system_program +/// - accounts[4]: light_token_program +pub fn process_approve_invoke_signed( + accounts: &[AccountInfo], + data: ApproveData, +) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the owner + let (pda, bump) = pinocchio::pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + + // Verify the owner account is the PDA we expect + if pda != *accounts[2].key() { + return Err(ProgramError::InvalidSeeds); + } + let bump_byte = [bump]; + let seeds = [Seed::from(TOKEN_ACCOUNT_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); + ApproveCpi { + token_account: &accounts[0], + delegate: &accounts[1], + owner: &accounts[2], + system_program: &accounts[3], + amount: data.amount, + } + .invoke_signed(&[signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/burn.rs b/sdk-tests/sdk-light-token-pinocchio/src/burn.rs new file mode 100644 index 0000000000..be576994ca --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/burn.rs @@ -0,0 +1,81 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_token_pinocchio::instruction::BurnCpi; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::{ID, TOKEN_ACCOUNT_SEED}; + +/// Instruction data for burn operations +#[derive(BorshSerialize, BorshDeserialize)] +pub struct BurnData { + pub amount: u64, +} + +/// Handler for burning CTokens (invoke) +/// +/// Account order: +/// - accounts[0]: source (Light Token account, writable) +/// - accounts[1]: mint (writable) +/// - accounts[2]: authority (owner, signer) +/// - accounts[3]: light_token_program +/// - accounts[4]: system_program +pub fn process_burn_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + BurnCpi { + source: &accounts[0], + mint: &accounts[1], + amount, + authority: &accounts[2], + system_program: &accounts[4], + fee_payer: None, + } + .invoke()?; + + Ok(()) +} + +/// Handler for burning CTokens with PDA authority (invoke_signed) +/// +/// Account order: +/// - accounts[0]: source (Light Token account, writable) +/// - accounts[1]: mint (writable) +/// - accounts[2]: PDA authority (owner, program signs) +/// - accounts[3]: light_token_program +/// - accounts[4]: system_program +pub fn process_burn_invoke_signed( + accounts: &[AccountInfo], + amount: u64, +) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the token account owner + let (pda, bump) = pinocchio::pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + + // Verify the authority account is the PDA we expect + if pda != *accounts[2].key() { + return Err(ProgramError::InvalidSeeds); + } + + let bump_byte = [bump]; + let seeds = [Seed::from(TOKEN_ACCOUNT_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); + BurnCpi { + source: &accounts[0], + mint: &accounts[1], + amount, + authority: &accounts[2], + system_program: &accounts[4], + fee_payer: None, + } + .invoke_signed(&[signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/close.rs b/sdk-tests/sdk-light-token-pinocchio/src/close.rs new file mode 100644 index 0000000000..e842caf4be --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/close.rs @@ -0,0 +1,69 @@ +use light_token_pinocchio::instruction::CloseAccountCpi; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::{ID, TOKEN_ACCOUNT_SEED}; + +/// Handler for closing a compressed token account (invoke) +/// +/// Account order: +/// - accounts[0]: token_program (ctoken program) +/// - accounts[1]: account to close (writable) +/// - accounts[2]: destination for lamports (writable) +/// - accounts[3]: owner/authority (signer) +/// - accounts[4]: rent_sponsor (writable) +pub fn process_close_account_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + CloseAccountCpi { + token_program: &accounts[0], + account: &accounts[1], + destination: &accounts[2], + owner: &accounts[3], + rent_sponsor: &accounts[4], + } + .invoke()?; + + Ok(()) +} + +/// Handler for closing a PDA-owned compressed token account (invoke_signed) +/// +/// Account order: +/// - accounts[0]: token_program (ctoken program) +/// - accounts[1]: account to close (writable) +/// - accounts[2]: destination for lamports (writable) +/// - accounts[3]: PDA owner/authority (not signer, program signs) +/// - accounts[4]: rent_sponsor (writable) +pub fn process_close_account_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the authority + let (pda, bump) = pinocchio::pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + + // Verify the authority account is the PDA we expect + if pda != *accounts[3].key() { + return Err(ProgramError::InvalidSeeds); + } + + let bump_byte = [bump]; + let seeds = [Seed::from(TOKEN_ACCOUNT_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); + CloseAccountCpi { + token_program: &accounts[0], + account: &accounts[1], + destination: &accounts[2], + owner: &accounts[3], + rent_sponsor: &accounts[4], + } + .invoke_signed(&[signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/create_ata.rs b/sdk-tests/sdk-light-token-pinocchio/src/create_ata.rs new file mode 100644 index 0000000000..ad36a0912b --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/create_ata.rs @@ -0,0 +1,95 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_token_pinocchio::instruction::CreateTokenAtaCpi; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +use crate::{ATA_SEED, ID}; + +/// Instruction data for create ATA (owner and mint passed as accounts) +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateAtaData { + pub bump: u8, + pub pre_pay_num_epochs: u8, + pub lamports_per_write: u32, +} + +/// Handler for creating a compressible associated token account (invoke) +/// +/// Account order: +/// - accounts[0]: owner +/// - accounts[1]: mint +/// - accounts[2]: payer (signer) +/// - accounts[3]: associated token account (derived) +/// - accounts[4]: system_program +/// - accounts[5]: compressible_config +/// - accounts[6]: rent_sponsor +pub fn process_create_ata_invoke( + accounts: &[AccountInfo], + data: CreateAtaData, +) -> Result<(), ProgramError> { + if accounts.len() < 7 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + CreateTokenAtaCpi { + payer: &accounts[2], + owner: &accounts[0], + mint: &accounts[1], + ata: &accounts[3], + bump: data.bump, + } + .rent_free( + &accounts[5], // compressible_config + &accounts[6], // rent_sponsor + &accounts[4], // system_program + ) + .invoke() + .map_err(|_| ProgramError::Custom(0))?; + + Ok(()) +} + +/// Handler for creating a compressible ATA with PDA ownership (invoke_signed) +/// +/// Account order: +/// - accounts[0]: owner +/// - accounts[1]: mint +/// - accounts[2]: payer (PDA, signer via invoke_signed) +/// - accounts[3]: associated token account (derived) +/// - accounts[4]: system_program +/// - accounts[5]: compressible_config +/// - accounts[6]: rent_sponsor +pub fn process_create_ata_invoke_signed( + accounts: &[AccountInfo], + data: CreateAtaData, +) -> Result<(), ProgramError> { + if accounts.len() < 7 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA that will act as payer/owner + let (pda, bump) = pinocchio::pubkey::find_program_address(&[ATA_SEED], &ID); + + // Verify the payer is the PDA + if pda != *accounts[2].key() { + return Err(ProgramError::InvalidSeeds); + } + + let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; + + CreateTokenAtaCpi { + payer: &accounts[2], + owner: &accounts[0], + mint: &accounts[1], + ata: &accounts[3], + bump: data.bump, + } + .rent_free( + &accounts[5], // compressible_config + &accounts[6], // rent_sponsor + &accounts[4], // system_program + ) + .invoke_signed(&[signer_seeds]) + .map_err(|_| ProgramError::Custom(0))?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/create_mint.rs b/sdk-tests/sdk-light-token-pinocchio/src/create_mint.rs new file mode 100644 index 0000000000..906ed27e9c --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/create_mint.rs @@ -0,0 +1,308 @@ +use alloc::vec::Vec; + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_token_pinocchio::instruction::{ + CompressedProof, CreateMintCpi, CreateMintParams, ExtensionInstructionData, SystemAccountInfos, +}; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::ID; + +/// PDA seed for mint signer in invoke_signed variant +pub const MINT_SIGNER_SEED: &[u8] = b"mint_signer"; + +/// Instruction data for create compressed mint +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateCmintData { + pub decimals: u8, + pub address_merkle_tree_root_index: u16, + pub mint_authority: [u8; 32], + pub proof: CompressedProof, + pub compression_address: [u8; 32], + pub mint: [u8; 32], + pub bump: u8, + pub freeze_authority: Option<[u8; 32]>, + pub extensions: Option>, + pub rent_payment: u8, + pub write_top_up: u32, +} + +/// Handler for creating a compressed mint (invoke) +/// +/// Uses the CreateMintCpi builder pattern. This demonstrates how to: +/// 1. Build the CreateMintParams struct from instruction data +/// 2. Build the CreateMintCpi with accounts +/// 3. Call invoke() which handles instruction building and CPI +/// +/// Account order (matches MintActionMetaConfig::to_account_metas()): +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: light_system_program +/// - accounts[2]: mint_signer (signer) +/// - accounts[3]: authority (signer) +/// - accounts[4]: compressible_config +/// - accounts[5]: mint (PDA, writable) +/// - accounts[6]: rent_sponsor (PDA, writable) +/// - accounts[7]: fee_payer (signer) +/// - accounts[8]: cpi_authority_pda +/// - accounts[9]: registered_program_pda +/// - accounts[10]: account_compression_authority +/// - accounts[11]: account_compression_program +/// - accounts[12]: system_program +/// - accounts[13]: output_queue +/// - accounts[14]: address_tree +/// - accounts[15] (optional): cpi_context_account +pub fn process_create_mint( + accounts: &[AccountInfo], + data: CreateCmintData, +) -> Result<(), ProgramError> { + if accounts.len() < 15 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Build the params + let params = CreateMintParams { + decimals: data.decimals, + address_merkle_tree_root_index: data.address_merkle_tree_root_index, + mint_authority: data.mint_authority, + proof: data.proof, + compression_address: data.compression_address, + mint: data.mint, + bump: data.bump, + freeze_authority: data.freeze_authority, + extensions: data.extensions, + rent_payment: data.rent_payment, + write_top_up: data.write_top_up, + }; + + // Build system accounts struct + let system_accounts = SystemAccountInfos { + light_system_program: &accounts[1], + cpi_authority_pda: &accounts[8], + registered_program_pda: &accounts[9], + account_compression_authority: &accounts[10], + account_compression_program: &accounts[11], + system_program: &accounts[12], + }; + + // Build the account infos struct + CreateMintCpi { + mint_seed: &accounts[2], + authority: &accounts[3], + payer: &accounts[7], + address_tree: &accounts[14], + output_queue: &accounts[13], + compressible_config: &accounts[4], + mint: &accounts[5], + rent_sponsor: &accounts[6], + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, + } + .invoke()?; + + Ok(()) +} + +/// Handler for creating a compressed mint with PDA mint signer (invoke_signed) +/// +/// Uses the CreateMintCpi builder pattern with invoke_signed. +/// The mint_signer is a PDA derived from this program. +/// +/// Account order (matches MintActionMetaConfig::to_account_metas()): +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: light_system_program +/// - accounts[2]: mint_signer (PDA, not signer - program signs) +/// - accounts[3]: authority (signer) +/// - accounts[4]: compressible_config +/// - accounts[5]: mint (PDA, writable) +/// - accounts[6]: rent_sponsor (PDA, writable) +/// - accounts[7]: fee_payer (signer) +/// - accounts[8]: cpi_authority_pda +/// - accounts[9]: registered_program_pda +/// - accounts[10]: account_compression_authority +/// - accounts[11]: account_compression_program +/// - accounts[12]: system_program +/// - accounts[13]: output_queue +/// - accounts[14]: address_tree +/// - accounts[15] (optional): cpi_context_account +pub fn process_create_mint_invoke_signed( + accounts: &[AccountInfo], + data: CreateCmintData, +) -> Result<(), ProgramError> { + if accounts.len() < 15 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the mint signer + let (pda, bump) = pinocchio::pubkey::find_program_address(&[MINT_SIGNER_SEED], &ID); + + // Verify the mint_signer account is the PDA we expect + if pda != *accounts[2].key() { + return Err(ProgramError::InvalidSeeds); + } + + // Build the params + let params = CreateMintParams { + decimals: data.decimals, + address_merkle_tree_root_index: data.address_merkle_tree_root_index, + mint_authority: data.mint_authority, + proof: data.proof, + compression_address: data.compression_address, + mint: data.mint, + bump: data.bump, + freeze_authority: data.freeze_authority, + extensions: data.extensions, + rent_payment: data.rent_payment, + write_top_up: data.write_top_up, + }; + + // Build system accounts struct + let system_accounts = SystemAccountInfos { + light_system_program: &accounts[1], + cpi_authority_pda: &accounts[8], + registered_program_pda: &accounts[9], + account_compression_authority: &accounts[10], + account_compression_program: &accounts[11], + system_program: &accounts[12], + }; + + // Build the account infos struct + let account_infos = CreateMintCpi { + mint_seed: &accounts[2], + authority: &accounts[3], + payer: &accounts[7], + address_tree: &accounts[14], + output_queue: &accounts[13], + compressible_config: &accounts[4], + mint: &accounts[5], + rent_sponsor: &accounts[6], + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, + }; + + // Invoke with PDA signing + let bump_byte = [bump]; + let seeds = [Seed::from(MINT_SIGNER_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); + account_infos.invoke_signed(&[signer])?; + + Ok(()) +} + +/// Handler for creating a compressed mint with PDA mint signer AND PDA authority (invoke_signed) +/// +/// Uses the SDK's CreateMintCpi with separate authority and payer accounts. +/// Both mint_signer and authority are PDAs signed by this program. +/// +/// Account order (matches MintActionMetaConfig::to_account_metas()): +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: light_system_program +/// - accounts[2]: mint_signer (PDA from MINT_SIGNER_SEED, not signer - program signs) +/// - accounts[3]: authority (PDA from MINT_AUTHORITY_SEED, not signer - program signs) +/// - accounts[4]: compressible_config +/// - accounts[5]: mint (PDA, writable) +/// - accounts[6]: rent_sponsor (PDA, writable) +/// - accounts[7]: fee_payer (signer) +/// - accounts[8]: cpi_authority_pda +/// - accounts[9]: registered_program_pda +/// - accounts[10]: account_compression_authority +/// - accounts[11]: account_compression_program +/// - accounts[12]: system_program +/// - accounts[13]: output_queue +/// - accounts[14]: address_tree +/// - accounts[15] (optional): cpi_context_account +pub fn process_create_mint_with_pda_authority( + accounts: &[AccountInfo], + data: CreateCmintData, +) -> Result<(), ProgramError> { + use crate::MINT_AUTHORITY_SEED; + + if accounts.len() < 15 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the mint signer + let (mint_signer_pda, mint_signer_bump) = + pinocchio::pubkey::find_program_address(&[MINT_SIGNER_SEED], &ID); + + // Derive the PDA for the authority + let (authority_pda, authority_bump) = + pinocchio::pubkey::find_program_address(&[MINT_AUTHORITY_SEED], &ID); + + // Verify the mint_signer account is the PDA we expect + if mint_signer_pda != *accounts[2].key() { + return Err(ProgramError::InvalidSeeds); + } + + // Verify the authority account is the PDA we expect + if authority_pda != *accounts[3].key() { + return Err(ProgramError::InvalidSeeds); + } + + // Build the params - authority is the PDA + let params = CreateMintParams { + decimals: data.decimals, + address_merkle_tree_root_index: data.address_merkle_tree_root_index, + mint_authority: authority_pda, + proof: data.proof, + compression_address: data.compression_address, + mint: data.mint, + bump: data.bump, + freeze_authority: data.freeze_authority, + extensions: data.extensions, + rent_payment: data.rent_payment, + write_top_up: data.write_top_up, + }; + + // Build system accounts struct + let system_accounts = SystemAccountInfos { + light_system_program: &accounts[1], + cpi_authority_pda: &accounts[8], + registered_program_pda: &accounts[9], + account_compression_authority: &accounts[10], + account_compression_program: &accounts[11], + system_program: &accounts[12], + }; + + // Build the account infos struct using SDK + let account_infos = CreateMintCpi { + mint_seed: &accounts[2], + authority: &accounts[3], + payer: &accounts[7], + address_tree: &accounts[14], + output_queue: &accounts[13], + compressible_config: &accounts[4], + mint: &accounts[5], + rent_sponsor: &accounts[6], + system_accounts, + cpi_context: None, + cpi_context_account: None, + params, + }; + + // Invoke with both PDAs signing + let mint_signer_bump_byte = [mint_signer_bump]; + let mint_signer_seeds = [ + Seed::from(MINT_SIGNER_SEED), + Seed::from(&mint_signer_bump_byte[..]), + ]; + let mint_signer = Signer::from(&mint_signer_seeds); + + let authority_bump_byte = [authority_bump]; + let authority_seeds = [ + Seed::from(MINT_AUTHORITY_SEED), + Seed::from(&authority_bump_byte[..]), + ]; + let authority_signer = Signer::from(&authority_seeds); + + account_infos.invoke_signed(&[mint_signer, authority_signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/create_token_account.rs b/sdk-tests/sdk-light-token-pinocchio/src/create_token_account.rs new file mode 100644 index 0000000000..27d157020a --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/create_token_account.rs @@ -0,0 +1,98 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_token_pinocchio::instruction::CreateTokenAccountCpi; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError}; + +use crate::{ID, TOKEN_ACCOUNT_SEED}; + +/// Instruction data for create token account +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct CreateTokenAccountData { + pub owner: [u8; 32], + pub pre_pay_num_epochs: u8, + pub lamports_per_write: u32, +} + +/// Handler for creating a compressible token account (invoke) +/// +/// Uses the builder pattern from the ctoken module. This demonstrates how to: +/// 1. Build the account infos struct with compressible params +/// 2. Call the invoke() method which handles instruction building and CPI +/// +/// Account order: +/// - accounts[0]: payer (signer) +/// - accounts[1]: account to create (signer) +/// - accounts[2]: mint +/// - accounts[3]: compressible_config +/// - accounts[4]: system_program +/// - accounts[5]: rent_sponsor +pub fn process_create_token_account_invoke( + accounts: &[AccountInfo], + data: CreateTokenAccountData, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Build the account infos struct and invoke with rent-free config + CreateTokenAccountCpi { + payer: &accounts[0], + account: &accounts[1], + mint: &accounts[2], + owner: data.owner, + } + .rent_free( + &accounts[3], // compressible_config + &accounts[5], // rent_sponsor + &accounts[4], // system_program + &ID, + ) + .invoke() + .map_err(|_| ProgramError::Custom(0))?; + + Ok(()) +} + +/// Handler for creating a compressible token account with PDA ownership (invoke_signed) +/// +/// Account order: +/// - accounts[0]: payer (signer) +/// - accounts[1]: account to create (PDA, will be derived and verified) +/// - accounts[2]: mint +/// - accounts[3]: compressible_config +/// - accounts[4]: system_program +/// - accounts[5]: rent_sponsor +pub fn process_create_token_account_invoke_signed( + accounts: &[AccountInfo], + data: CreateTokenAccountData, +) -> Result<(), ProgramError> { + if accounts.len() < 6 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the token account + let (pda, bump) = pinocchio::pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + + // Verify the account to create is the PDA + if pda != *accounts[1].key() { + return Err(ProgramError::InvalidSeeds); + } + + // Invoke with PDA signing and rent-free config + let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; + CreateTokenAccountCpi { + payer: &accounts[0], + account: &accounts[1], + mint: &accounts[2], + owner: data.owner, + } + .rent_free( + &accounts[3], // compressible_config + &accounts[5], // rent_sponsor + &accounts[4], // system_program + &ID, + ) + .invoke_signed(signer_seeds) + .map_err(|_| ProgramError::Custom(0))?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs b/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs new file mode 100644 index 0000000000..95920489aa --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/ctoken_mint_to.rs @@ -0,0 +1,82 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_token_pinocchio::instruction::MintToCpi; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::{ID, MINT_AUTHORITY_SEED}; + +/// Instruction data for MintTo operations +#[derive(BorshSerialize, BorshDeserialize)] +pub struct MintToData { + pub amount: u64, +} + +/// Handler for minting to Token (invoke) +/// +/// Account order: +/// - accounts[0]: mint (writable) +/// - accounts[1]: destination (Token account, writable) +/// - accounts[2]: authority (mint authority, signer) +/// - accounts[3]: system_program +/// - accounts[4]: light_token_program +pub fn process_mint_to_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + MintToCpi { + mint: &accounts[0], + destination: &accounts[1], + amount, + authority: &accounts[2], + system_program: &accounts[3], + fee_payer: None, + } + .invoke()?; + + Ok(()) +} + +/// Handler for minting to Token with PDA authority (invoke_signed) +/// +/// Account order: +/// - accounts[0]: mint (writable) +/// - accounts[1]: destination (Token account, writable) +/// - accounts[2]: PDA authority (mint authority, program signs) +/// - accounts[3]: system_program +/// - accounts[4]: light_token_program +pub fn process_mint_to_invoke_signed( + accounts: &[AccountInfo], + amount: u64, +) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the mint authority + let (pda, bump) = pinocchio::pubkey::find_program_address(&[MINT_AUTHORITY_SEED], &ID); + + // Verify the authority account is the PDA we expect + if pda != *accounts[2].key() { + return Err(ProgramError::InvalidSeeds); + } + + let bump_byte = [bump]; + let seeds = [Seed::from(MINT_AUTHORITY_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); + + MintToCpi { + mint: &accounts[0], + destination: &accounts[1], + amount, + authority: &accounts[2], + system_program: &accounts[3], + fee_payer: None, + } + .invoke_signed(&[signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/freeze.rs b/sdk-tests/sdk-light-token-pinocchio/src/freeze.rs new file mode 100644 index 0000000000..3f93b6abd4 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/freeze.rs @@ -0,0 +1,67 @@ +use light_token_pinocchio::instruction::FreezeCpi; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::{FREEZE_AUTHORITY_SEED, ID}; + +/// Handler for freezing a Light Token account (invoke) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: mint +/// - accounts[2]: freeze_authority (signer) +/// - accounts[3]: light_token_program +pub fn process_freeze_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + FreezeCpi { + token_account: &accounts[0], + mint: &accounts[1], + freeze_authority: &accounts[2], + } + .invoke()?; + + Ok(()) +} + +/// Handler for freezing a Light Token account with PDA freeze authority (invoke_signed) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: mint +/// - accounts[2]: PDA freeze_authority (program signs) +/// - accounts[3]: light_token_program +pub fn process_freeze_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the freeze authority + let (pda, bump) = pinocchio::pubkey::find_program_address(&[FREEZE_AUTHORITY_SEED], &ID); + + // Verify the freeze_authority account is the PDA we expect + if pda != *accounts[2].key() { + return Err(ProgramError::InvalidSeeds); + } + + let bump_byte = [bump]; + let seeds = [ + Seed::from(FREEZE_AUTHORITY_SEED), + Seed::from(&bump_byte[..]), + ]; + let signer = Signer::from(&seeds); + + FreezeCpi { + token_account: &accounts[0], + mint: &accounts[1], + freeze_authority: &accounts[2], + } + .invoke_signed(&[signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/lib.rs b/sdk-tests/sdk-light-token-pinocchio/src/lib.rs new file mode 100644 index 0000000000..e3abcde6ed --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/lib.rs @@ -0,0 +1,326 @@ +#![allow(unexpected_cfgs)] +#![no_std] + +extern crate alloc; + +mod approve; +mod burn; +mod close; +mod create_ata; +mod create_mint; +mod create_token_account; +mod ctoken_mint_to; +mod freeze; +mod revoke; +mod thaw; +mod transfer; +mod transfer_checked; +mod transfer_interface; +mod transfer_spl_ctoken; + +// Re-export all instruction data types +pub use approve::{process_approve_invoke, process_approve_invoke_signed, ApproveData}; +pub use burn::{process_burn_invoke, process_burn_invoke_signed, BurnData}; +pub use close::{process_close_account_invoke, process_close_account_invoke_signed}; +pub use create_ata::{process_create_ata_invoke, process_create_ata_invoke_signed, CreateAtaData}; +pub use create_mint::{ + process_create_mint, process_create_mint_invoke_signed, process_create_mint_with_pda_authority, + CreateCmintData, MINT_SIGNER_SEED, +}; +pub use create_token_account::{ + process_create_token_account_invoke, process_create_token_account_invoke_signed, + CreateTokenAccountData, +}; +pub use ctoken_mint_to::{process_mint_to_invoke, process_mint_to_invoke_signed, MintToData}; +pub use freeze::{process_freeze_invoke, process_freeze_invoke_signed}; +use light_macros::pubkey_array; +use pinocchio::{ + account_info::AccountInfo, entrypoint, program_error::ProgramError, ProgramResult, +}; +pub use revoke::{process_revoke_invoke, process_revoke_invoke_signed}; +pub use thaw::{process_thaw_invoke, process_thaw_invoke_signed}; +pub use transfer::{process_transfer_invoke, process_transfer_invoke_signed, TransferData}; +pub use transfer_checked::{ + process_transfer_checked_invoke, process_transfer_checked_invoke_signed, TransferCheckedData, +}; +pub use transfer_interface::{ + process_transfer_interface_invoke, process_transfer_interface_invoke_signed, + TransferInterfaceData, TRANSFER_INTERFACE_AUTHORITY_SEED, +}; +pub use transfer_spl_ctoken::{ + process_ctoken_to_spl_invoke, process_ctoken_to_spl_invoke_signed, + process_spl_to_ctoken_invoke, process_spl_to_ctoken_invoke_signed, TransferFromSplData, + TransferTokenToSplData, TRANSFER_AUTHORITY_SEED, +}; + +/// Program ID - replace with actual program ID after deployment +pub const ID: [u8; 32] = pubkey_array!("CToknNtvExmp1eProgram11111111111111111111112"); + +/// PDA seeds for invoke_signed instructions +pub const TOKEN_ACCOUNT_SEED: &[u8] = b"token_account"; +pub const ATA_SEED: &[u8] = b"ata"; +pub const FREEZE_AUTHORITY_SEED: &[u8] = b"freeze_authority"; +pub const MINT_AUTHORITY_SEED: &[u8] = b"mint_authority"; + +entrypoint!(process_instruction); + +/// Instruction discriminators +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InstructionType { + /// Create a compressed mint + CreateCmint = 0, + /// Create compressible token account (invoke) + CreateTokenAccountInvoke = 2, + /// Create compressible token account with PDA ownership (invoke_signed) + CreateTokenAccountInvokeSigned = 3, + /// Create compressible associated token account (invoke) + CreateAtaInvoke = 4, + /// Create compressible associated token account with PDA ownership (invoke_signed) + CreateAtaInvokeSigned = 5, + /// Transfer compressed tokens Light Token->Light Token (invoke) + CTokenTransferInvoke = 6, + /// Transfer compressed tokens Light Token->Light Token from PDA-owned account (invoke_signed) + CTokenTransferInvokeSigned = 7, + /// Close compressed token account (invoke) + CloseAccountInvoke = 8, + /// Close PDA-owned compressed token account (invoke_signed) + CloseAccountInvokeSigned = 9, + /// Create ATA using V2 variant (invoke) + CreateAta2Invoke = 10, + /// Create ATA using V2 variant with PDA ownership (invoke_signed) + CreateAta2InvokeSigned = 11, + /// Create a compressed mint with PDA mint signer (invoke_signed) + CreateCmintInvokeSigned = 12, + /// Create a compressed mint with PDA mint signer AND PDA authority (invoke_signed) + CreateCmintWithPdaAuthority = 14, + /// Transfer SPL tokens to Light Token account (invoke) + SplToCtokenInvoke = 15, + /// Transfer SPL tokens to Light Token account with PDA authority (invoke_signed) + SplToCtokenInvokeSigned = 16, + /// Transfer Light Token to SPL token account (invoke) + CtokenToSplInvoke = 17, + /// Transfer Light Token to SPL token account with PDA authority (invoke_signed) + CtokenToSplInvokeSigned = 18, + /// Unified transfer interface - auto-detects account types (invoke) + TransferInterfaceInvoke = 19, + /// Unified transfer interface with PDA authority (invoke_signed) + TransferInterfaceInvokeSigned = 20, + /// Approve delegate for Light Token account (invoke) + ApproveInvoke = 21, + /// Approve delegate for PDA-owned Light Token account (invoke_signed) + ApproveInvokeSigned = 22, + /// Revoke delegation for Light Token account (invoke) + RevokeInvoke = 23, + /// Revoke delegation for PDA-owned Light Token account (invoke_signed) + RevokeInvokeSigned = 24, + /// Freeze Light Token account (invoke) + FreezeInvoke = 25, + /// Freeze Light Token account with PDA freeze authority (invoke_signed) + FreezeInvokeSigned = 26, + /// Thaw frozen Light Token account (invoke) + ThawInvoke = 27, + /// Thaw frozen Light Token account with PDA freeze authority (invoke_signed) + ThawInvokeSigned = 28, + /// Burn CTokens (invoke) + BurnInvoke = 29, + /// Burn CTokens with PDA authority (invoke_signed) + BurnInvokeSigned = 30, + /// Mint to Light Token from decompressed Mint (invoke) + CTokenMintToInvoke = 31, + /// Mint to Light Token from decompressed Mint with PDA authority (invoke_signed) + CTokenMintToInvokeSigned = 32, + /// Transfer cTokens with checked decimals (invoke) + CTokenTransferCheckedInvoke = 34, + /// Transfer cTokens with checked decimals from PDA-owned account (invoke_signed) + CTokenTransferCheckedInvokeSigned = 35, +} + +impl TryFrom for InstructionType { + type Error = ProgramError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(InstructionType::CreateCmint), + 2 => Ok(InstructionType::CreateTokenAccountInvoke), + 3 => Ok(InstructionType::CreateTokenAccountInvokeSigned), + 4 => Ok(InstructionType::CreateAtaInvoke), + 5 => Ok(InstructionType::CreateAtaInvokeSigned), + 6 => Ok(InstructionType::CTokenTransferInvoke), + 7 => Ok(InstructionType::CTokenTransferInvokeSigned), + 8 => Ok(InstructionType::CloseAccountInvoke), + 9 => Ok(InstructionType::CloseAccountInvokeSigned), + 10 => Ok(InstructionType::CreateAta2Invoke), + 11 => Ok(InstructionType::CreateAta2InvokeSigned), + 12 => Ok(InstructionType::CreateCmintInvokeSigned), + 14 => Ok(InstructionType::CreateCmintWithPdaAuthority), + 15 => Ok(InstructionType::SplToCtokenInvoke), + 16 => Ok(InstructionType::SplToCtokenInvokeSigned), + 17 => Ok(InstructionType::CtokenToSplInvoke), + 18 => Ok(InstructionType::CtokenToSplInvokeSigned), + 19 => Ok(InstructionType::TransferInterfaceInvoke), + 20 => Ok(InstructionType::TransferInterfaceInvokeSigned), + 21 => Ok(InstructionType::ApproveInvoke), + 22 => Ok(InstructionType::ApproveInvokeSigned), + 23 => Ok(InstructionType::RevokeInvoke), + 24 => Ok(InstructionType::RevokeInvokeSigned), + 25 => Ok(InstructionType::FreezeInvoke), + 26 => Ok(InstructionType::FreezeInvokeSigned), + 27 => Ok(InstructionType::ThawInvoke), + 28 => Ok(InstructionType::ThawInvokeSigned), + 29 => Ok(InstructionType::BurnInvoke), + 30 => Ok(InstructionType::BurnInvokeSigned), + 31 => Ok(InstructionType::CTokenMintToInvoke), + 32 => Ok(InstructionType::CTokenMintToInvokeSigned), + 34 => Ok(InstructionType::CTokenTransferCheckedInvoke), + 35 => Ok(InstructionType::CTokenTransferCheckedInvokeSigned), + _ => Err(ProgramError::InvalidInstructionData), + } + } +} + +/// Main program entrypoint +pub fn process_instruction( + program_id: &[u8; 32], + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + use borsh::BorshDeserialize; + + if *program_id != ID { + return Err(ProgramError::IncorrectProgramId); + } + + if instruction_data.is_empty() { + return Err(ProgramError::InvalidInstructionData); + } + + let discriminator = InstructionType::try_from(instruction_data[0])?; + + match discriminator { + InstructionType::CreateCmint => { + let data = CreateCmintData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_mint(accounts, data) + } + InstructionType::CreateTokenAccountInvoke => { + let data = CreateTokenAccountData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_token_account_invoke(accounts, data) + } + InstructionType::CreateTokenAccountInvokeSigned => { + let data = CreateTokenAccountData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_token_account_invoke_signed(accounts, data) + } + InstructionType::CreateAtaInvoke => { + let data = CreateAtaData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_ata_invoke(accounts, data) + } + InstructionType::CreateAtaInvokeSigned => { + let data = CreateAtaData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_ata_invoke_signed(accounts, data) + } + InstructionType::CTokenTransferInvoke => { + let data = TransferData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_transfer_invoke(accounts, data) + } + InstructionType::CTokenTransferInvokeSigned => { + let data = TransferData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_transfer_invoke_signed(accounts, data) + } + InstructionType::CloseAccountInvoke => process_close_account_invoke(accounts), + InstructionType::CloseAccountInvokeSigned => process_close_account_invoke_signed(accounts), + InstructionType::CreateCmintInvokeSigned => { + let data = CreateCmintData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_mint_invoke_signed(accounts, data) + } + InstructionType::CreateCmintWithPdaAuthority => { + let data = CreateCmintData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_create_mint_with_pda_authority(accounts, data) + } + InstructionType::SplToCtokenInvoke => { + let data = TransferFromSplData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_spl_to_ctoken_invoke(accounts, data) + } + InstructionType::SplToCtokenInvokeSigned => { + let data = TransferFromSplData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_spl_to_ctoken_invoke_signed(accounts, data) + } + InstructionType::CtokenToSplInvoke => { + let data = TransferTokenToSplData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_ctoken_to_spl_invoke(accounts, data) + } + InstructionType::CtokenToSplInvokeSigned => { + let data = TransferTokenToSplData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_ctoken_to_spl_invoke_signed(accounts, data) + } + InstructionType::TransferInterfaceInvoke => { + let data = TransferInterfaceData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_transfer_interface_invoke(accounts, data) + } + InstructionType::TransferInterfaceInvokeSigned => { + let data = TransferInterfaceData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_transfer_interface_invoke_signed(accounts, data) + } + InstructionType::ApproveInvoke => { + let data = ApproveData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_approve_invoke(accounts, data) + } + InstructionType::ApproveInvokeSigned => { + let data = ApproveData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_approve_invoke_signed(accounts, data) + } + InstructionType::RevokeInvoke => process_revoke_invoke(accounts), + InstructionType::RevokeInvokeSigned => process_revoke_invoke_signed(accounts), + InstructionType::FreezeInvoke => process_freeze_invoke(accounts), + InstructionType::FreezeInvokeSigned => process_freeze_invoke_signed(accounts), + InstructionType::ThawInvoke => process_thaw_invoke(accounts), + InstructionType::ThawInvokeSigned => process_thaw_invoke_signed(accounts), + InstructionType::BurnInvoke => { + let data = BurnData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_burn_invoke(accounts, data.amount) + } + InstructionType::BurnInvokeSigned => { + let data = BurnData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_burn_invoke_signed(accounts, data.amount) + } + InstructionType::CTokenMintToInvoke => { + let data = MintToData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_mint_to_invoke(accounts, data.amount) + } + InstructionType::CTokenMintToInvokeSigned => { + let data = MintToData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_mint_to_invoke_signed(accounts, data.amount) + } + InstructionType::CTokenTransferCheckedInvoke => { + let data = TransferCheckedData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_transfer_checked_invoke(accounts, data) + } + InstructionType::CTokenTransferCheckedInvokeSigned => { + let data = TransferCheckedData::try_from_slice(&instruction_data[1..]) + .map_err(|_| ProgramError::InvalidInstructionData)?; + process_transfer_checked_invoke_signed(accounts, data) + } + _ => Err(ProgramError::InvalidInstructionData), + } +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs new file mode 100644 index 0000000000..a1fb1508f9 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/revoke.rs @@ -0,0 +1,64 @@ +use light_token_pinocchio::instruction::RevokeCpi; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::{ID, TOKEN_ACCOUNT_SEED}; + +/// Handler for revoking delegation on a Light Token account (invoke) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: owner (signer) +/// - accounts[2]: system_program +/// - accounts[3]: light_token_program +pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + RevokeCpi { + token_account: &accounts[0], + owner: &accounts[1], + system_program: &accounts[2], + } + .invoke()?; + + Ok(()) +} + +/// Handler for revoking delegation on a PDA-owned Light Token account (invoke_signed) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: PDA owner (program signs) +/// - accounts[2]: system_program +/// - accounts[3]: light_token_program +pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the owner + let (pda, bump) = pinocchio::pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + + // Verify the owner account is the PDA we expect + if pda != *accounts[1].key() { + return Err(ProgramError::InvalidSeeds); + } + + let bump_byte = [bump]; + let seeds = [Seed::from(TOKEN_ACCOUNT_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); + + RevokeCpi { + token_account: &accounts[0], + owner: &accounts[1], + system_program: &accounts[2], + } + .invoke_signed(&[signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/thaw.rs b/sdk-tests/sdk-light-token-pinocchio/src/thaw.rs new file mode 100644 index 0000000000..6d6170a2f9 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/thaw.rs @@ -0,0 +1,67 @@ +use light_token_pinocchio::instruction::ThawCpi; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::{FREEZE_AUTHORITY_SEED, ID}; + +/// Handler for thawing a frozen Light Token account (invoke) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: mint +/// - accounts[2]: freeze_authority (signer) +/// - accounts[3]: light_token_program +pub fn process_thaw_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + ThawCpi { + token_account: &accounts[0], + mint: &accounts[1], + freeze_authority: &accounts[2], + } + .invoke()?; + + Ok(()) +} + +/// Handler for thawing a frozen Light Token account with PDA freeze authority (invoke_signed) +/// +/// Account order: +/// - accounts[0]: token_account (writable) +/// - accounts[1]: mint +/// - accounts[2]: PDA freeze_authority (program signs) +/// - accounts[3]: light_token_program +pub fn process_thaw_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the freeze authority + let (pda, bump) = pinocchio::pubkey::find_program_address(&[FREEZE_AUTHORITY_SEED], &ID); + + // Verify the freeze_authority account is the PDA we expect + if pda != *accounts[2].key() { + return Err(ProgramError::InvalidSeeds); + } + + let bump_byte = [bump]; + let seeds = [ + Seed::from(FREEZE_AUTHORITY_SEED), + Seed::from(&bump_byte[..]), + ]; + let signer = Signer::from(&seeds); + + ThawCpi { + token_account: &accounts[0], + mint: &accounts[1], + freeze_authority: &accounts[2], + } + .invoke_signed(&[signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs new file mode 100644 index 0000000000..4958e1faa3 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/transfer.rs @@ -0,0 +1,95 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_token_pinocchio::instruction::TransferCpi; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::{ID, TOKEN_ACCOUNT_SEED}; + +/// Instruction data for transfer operations +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct TransferData { + pub amount: u64, +} + +/// Handler for transferring compressed tokens (invoke) +/// +/// Uses the builder pattern from the ctoken module. This demonstrates how to: +/// 1. Build the account infos struct +/// 2. Call the invoke() method which handles instruction building and CPI +/// +/// Account order: +/// - accounts[0]: source ctoken account +/// - accounts[1]: destination ctoken account +/// - accounts[2]: authority (signer) +/// - accounts[3]: system_program +pub fn process_transfer_invoke( + accounts: &[AccountInfo], + data: TransferData, +) -> Result<(), ProgramError> { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Build the account infos struct using the builder pattern + TransferCpi { + source: &accounts[0], + destination: &accounts[1], + amount: data.amount, + authority: &accounts[2], + system_program: &accounts[3], + fee_payer: None, + } + .invoke()?; + + Ok(()) +} + +/// Handler for transferring compressed tokens from PDA-owned account (invoke_signed) +/// +/// Uses the builder pattern with invoke_signed. This demonstrates how to: +/// 1. Build the account infos struct +/// 2. Derive PDA seeds +/// 3. Call invoke_signed() method with the signer seeds +/// +/// Account order: +/// - accounts[0]: source ctoken account (PDA-owned) +/// - accounts[1]: destination ctoken account +/// - accounts[2]: authority (PDA) +/// - accounts[3]: system_program +pub fn process_transfer_invoke_signed( + accounts: &[AccountInfo], + data: TransferData, +) -> Result<(), ProgramError> { + if accounts.len() < 4 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the authority + let (pda, bump) = pinocchio::pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + + // Verify the authority account is the PDA we expect + if pda != *accounts[2].key() { + return Err(ProgramError::InvalidSeeds); + } + + // Build the account infos struct + let transfer_accounts = TransferCpi { + source: &accounts[0], + destination: &accounts[1], + amount: data.amount, + authority: &accounts[2], + system_program: &accounts[3], + fee_payer: None, + }; + + // Invoke with PDA signing - the builder handles instruction creation and invoke_signed CPI + let bump_byte = [bump]; + let seeds = [Seed::from(TOKEN_ACCOUNT_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); + transfer_accounts.invoke_signed(&[signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs new file mode 100644 index 0000000000..7bf130bce1 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/transfer_checked.rs @@ -0,0 +1,91 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_token_pinocchio::instruction::TransferCheckedCpi; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::{ID, TOKEN_ACCOUNT_SEED}; + +/// Instruction data for transfer_checked operations +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct TransferCheckedData { + pub amount: u64, + pub decimals: u8, +} + +/// Handler for transferring cTokens with checked decimals (invoke) +/// +/// Account order: +/// - accounts[0]: source ctoken account +/// - accounts[1]: mint (SPL, T22, or decompressed Mint) +/// - accounts[2]: destination ctoken account +/// - accounts[3]: authority (signer) +/// - accounts[4]: system_program +pub fn process_transfer_checked_invoke( + accounts: &[AccountInfo], + data: TransferCheckedData, +) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + TransferCheckedCpi { + source: &accounts[0], + mint: &accounts[1], + destination: &accounts[2], + amount: data.amount, + decimals: data.decimals, + authority: &accounts[3], + system_program: &accounts[4], + fee_payer: None, + } + .invoke()?; + + Ok(()) +} + +/// Handler for transferring cTokens with checked decimals from PDA-owned account (invoke_signed) +/// +/// Account order: +/// - accounts[0]: source ctoken account (PDA-owned) +/// - accounts[1]: mint (SPL, T22, or decompressed Mint) +/// - accounts[2]: destination ctoken account +/// - accounts[3]: authority (PDA) +/// - accounts[4]: system_program +pub fn process_transfer_checked_invoke_signed( + accounts: &[AccountInfo], + data: TransferCheckedData, +) -> Result<(), ProgramError> { + if accounts.len() < 5 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the authority + let (pda, bump) = pinocchio::pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &ID); + + // Verify the authority account is the PDA we expect + if pda != *accounts[3].key() { + return Err(ProgramError::InvalidSeeds); + } + + let transfer_accounts = TransferCheckedCpi { + source: &accounts[0], + mint: &accounts[1], + destination: &accounts[2], + amount: data.amount, + decimals: data.decimals, + authority: &accounts[3], + system_program: &accounts[4], + fee_payer: None, + }; + + // Invoke with PDA signing + let bump_byte = [bump]; + let seeds = [Seed::from(TOKEN_ACCOUNT_SEED), Seed::from(&bump_byte[..])]; + let signer = Signer::from(&seeds); + transfer_accounts.invoke_signed(&[signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/transfer_interface.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer_interface.rs new file mode 100644 index 0000000000..29895480ed --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/transfer_interface.rs @@ -0,0 +1,140 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_token_pinocchio::instruction::{SplInterfaceCpi, TransferInterfaceCpi}; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::ID; + +/// PDA seed for authority in invoke_signed variants +pub const TRANSFER_INTERFACE_AUTHORITY_SEED: &[u8] = b"transfer_interface_authority"; + +/// Instruction data for TransferInterface +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct TransferInterfaceData { + pub amount: u64, + pub decimals: u8, + /// Required for SPL<->Light Token transfers, None for Light Token->Light Token + pub spl_interface_pda_bump: Option, +} + +/// Handler for TransferInterface (invoke) +/// +/// This unified interface automatically detects account types and routes to: +/// - Light Token -> Light Token transfer +/// - Light Token -> SPL transfer +/// - SPL -> Light Token transfer +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: source_account (SPL or Light Token) +/// - accounts[2]: destination_account (SPL or Light Token) +/// - accounts[3]: authority (signer) +/// - accounts[4]: payer (signer) +/// - accounts[5]: compressed_token_program_authority +/// - accounts[6]: system_program +/// For SPL bridge (optional, required for SPL<->Light Token): +/// - accounts[7]: mint +/// - accounts[8]: spl_interface_pda +/// - accounts[9]: spl_token_program +pub fn process_transfer_interface_invoke( + accounts: &[AccountInfo], + data: TransferInterfaceData, +) -> Result<(), ProgramError> { + if accounts.len() < 7 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + let mut transfer = TransferInterfaceCpi::new( + data.amount, + data.decimals, + &accounts[1], // source_account + &accounts[2], // destination_account + &accounts[3], // authority + &accounts[4], // payer + &accounts[5], // compressed_token_program_authority + &accounts[6], // system_program + ); + + // Add SPL bridge config if provided + if accounts.len() >= 10 && data.spl_interface_pda_bump.is_some() { + transfer = transfer.with_spl_interface(SplInterfaceCpi { + mint: &accounts[7], + spl_token_program: &accounts[9], + spl_interface_pda: &accounts[8], + spl_interface_pda_bump: data.spl_interface_pda_bump.unwrap(), + }); + } + + transfer.invoke()?; + + Ok(()) +} + +/// Handler for TransferInterfaceCpi with PDA authority (invoke_signed) +/// +/// The authority is a PDA derived from TRANSFER_INTERFACE_AUTHORITY_SEED. +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: source_account (SPL or Light Token) +/// - accounts[2]: destination_account (SPL or Light Token) +/// - accounts[3]: authority (PDA, not signer - program signs) +/// - accounts[4]: payer (signer) +/// - accounts[5]: compressed_token_program_authority +/// - accounts[6]: system_program +/// For SPL bridge (optional, required for SPL<->Light Token): +/// - accounts[7]: mint +/// - accounts[8]: spl_interface_pda +/// - accounts[9]: spl_token_program +pub fn process_transfer_interface_invoke_signed( + accounts: &[AccountInfo], + data: TransferInterfaceData, +) -> Result<(), ProgramError> { + if accounts.len() < 7 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the authority + let (authority_pda, authority_bump) = + pinocchio::pubkey::find_program_address(&[TRANSFER_INTERFACE_AUTHORITY_SEED], &ID); + + // Verify the authority account is the PDA we expect + if authority_pda != *accounts[3].key() { + return Err(ProgramError::InvalidSeeds); + } + + let mut transfer = TransferInterfaceCpi::new( + data.amount, + data.decimals, + &accounts[1], // source_account + &accounts[2], // destination_account + &accounts[3], // authority (PDA) + &accounts[4], // payer + &accounts[5], // compressed_token_program_authority + &accounts[6], // system_program + ); + + // Add SPL bridge config if provided + if accounts.len() >= 10 && data.spl_interface_pda_bump.is_some() { + transfer = transfer.with_spl_interface(SplInterfaceCpi { + mint: &accounts[7], + spl_token_program: &accounts[9], + spl_interface_pda: &accounts[8], + spl_interface_pda_bump: data.spl_interface_pda_bump.unwrap(), + }); + } + + // Invoke with PDA signing + let authority_bump_byte = [authority_bump]; + let authority_seeds = [ + Seed::from(TRANSFER_INTERFACE_AUTHORITY_SEED), + Seed::from(&authority_bump_byte[..]), + ]; + let authority_signer = Signer::from(&authority_seeds); + transfer.invoke_signed(&[authority_signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/src/transfer_spl_ctoken.rs b/sdk-tests/sdk-light-token-pinocchio/src/transfer_spl_ctoken.rs new file mode 100644 index 0000000000..a13568cfbf --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/src/transfer_spl_ctoken.rs @@ -0,0 +1,222 @@ +use borsh::{BorshDeserialize, BorshSerialize}; +use light_token_pinocchio::instruction::{TransferFromSplCpi, TransferToSplCpi}; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, +}; + +use crate::ID; + +/// PDA seed for authority in invoke_signed variants +pub const TRANSFER_AUTHORITY_SEED: &[u8] = b"transfer_authority"; + +/// Instruction data for SPL to Light Token transfer +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct TransferFromSplData { + pub amount: u64, + pub spl_interface_pda_bump: u8, + pub decimals: u8, +} + +/// Instruction data for Light Token to SPL transfer +#[derive(BorshSerialize, BorshDeserialize, Debug)] +pub struct TransferTokenToSplData { + pub amount: u64, + pub spl_interface_pda_bump: u8, + pub decimals: u8, +} + +/// Handler for transferring SPL tokens to Light Token (invoke) +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: source_spl_token_account +/// - accounts[2]: destination (writable) +/// - accounts[3]: authority (signer) +/// - accounts[4]: mint +/// - accounts[5]: payer (signer) +/// - accounts[6]: spl_interface_pda +/// - accounts[7]: spl_token_program +/// - accounts[8]: compressed_token_program_authority +/// - accounts[9]: system_program +pub fn process_spl_to_ctoken_invoke( + accounts: &[AccountInfo], + data: TransferFromSplData, +) -> Result<(), ProgramError> { + if accounts.len() < 10 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + TransferFromSplCpi { + source_spl_token_account: &accounts[1], + destination: &accounts[2], + amount: data.amount, + authority: &accounts[3], + mint: &accounts[4], + payer: &accounts[5], + spl_interface_pda: &accounts[6], + spl_interface_pda_bump: data.spl_interface_pda_bump, + decimals: data.decimals, + spl_token_program: &accounts[7], + compressed_token_program_authority: &accounts[8], + system_program: &accounts[9], + } + .invoke()?; + + Ok(()) +} + +/// Handler for transferring SPL tokens to Light Token with PDA authority (invoke_signed) +/// +/// The authority is a PDA derived from TRANSFER_AUTHORITY_SEED. +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: source_spl_token_account +/// - accounts[2]: destination (writable) +/// - accounts[3]: authority (PDA, not signer - program signs) +/// - accounts[4]: mint +/// - accounts[5]: payer (signer) +/// - accounts[6]: spl_interface_pda +/// - accounts[7]: spl_token_program +/// - accounts[8]: compressed_token_program_authority +/// - accounts[9]: system_program +pub fn process_spl_to_ctoken_invoke_signed( + accounts: &[AccountInfo], + data: TransferFromSplData, +) -> Result<(), ProgramError> { + if accounts.len() < 10 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the authority + let (authority_pda, authority_bump) = + pinocchio::pubkey::find_program_address(&[TRANSFER_AUTHORITY_SEED], &ID); + + // Verify the authority account is the PDA we expect + if authority_pda != *accounts[3].key() { + return Err(ProgramError::InvalidSeeds); + } + + let account_infos = TransferFromSplCpi { + source_spl_token_account: &accounts[1], + destination: &accounts[2], + amount: data.amount, + authority: &accounts[3], + mint: &accounts[4], + payer: &accounts[5], + spl_interface_pda: &accounts[6], + spl_interface_pda_bump: data.spl_interface_pda_bump, + decimals: data.decimals, + spl_token_program: &accounts[7], + compressed_token_program_authority: &accounts[8], + system_program: &accounts[9], + }; + + // Invoke with PDA signing + let authority_bump_byte = [authority_bump]; + let authority_seeds = [ + Seed::from(TRANSFER_AUTHORITY_SEED), + Seed::from(&authority_bump_byte[..]), + ]; + let authority_signer = Signer::from(&authority_seeds); + account_infos.invoke_signed(&[authority_signer])?; + + Ok(()) +} + +/// Handler for transferring Light Token to SPL tokens (invoke) +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: source +/// - accounts[2]: destination_spl_token_account +/// - accounts[3]: authority (signer) +/// - accounts[4]: mint +/// - accounts[5]: payer (signer) +/// - accounts[6]: spl_interface_pda +/// - accounts[7]: spl_token_program +/// - accounts[8]: compressed_token_program_authority +pub fn process_ctoken_to_spl_invoke( + accounts: &[AccountInfo], + data: TransferTokenToSplData, +) -> Result<(), ProgramError> { + if accounts.len() < 9 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + TransferToSplCpi { + source: &accounts[1], + destination_spl_token_account: &accounts[2], + amount: data.amount, + authority: &accounts[3], + mint: &accounts[4], + payer: &accounts[5], + spl_interface_pda: &accounts[6], + spl_interface_pda_bump: data.spl_interface_pda_bump, + decimals: data.decimals, + spl_token_program: &accounts[7], + compressed_token_program_authority: &accounts[8], + } + .invoke()?; + + Ok(()) +} + +/// Handler for transferring Light Token to SPL tokens with PDA authority (invoke_signed) +/// +/// The authority is a PDA derived from TRANSFER_AUTHORITY_SEED. +/// +/// Account order: +/// - accounts[0]: compressed_token_program (for CPI) +/// - accounts[1]: source +/// - accounts[2]: destination_spl_token_account +/// - accounts[3]: authority (PDA, not signer - program signs) +/// - accounts[4]: mint +/// - accounts[5]: payer (signer) +/// - accounts[6]: spl_interface_pda +/// - accounts[7]: spl_token_program +/// - accounts[8]: compressed_token_program_authority +pub fn process_ctoken_to_spl_invoke_signed( + accounts: &[AccountInfo], + data: TransferTokenToSplData, +) -> Result<(), ProgramError> { + if accounts.len() < 9 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + // Derive the PDA for the authority + let (authority_pda, authority_bump) = + pinocchio::pubkey::find_program_address(&[TRANSFER_AUTHORITY_SEED], &ID); + + // Verify the authority account is the PDA we expect + if authority_pda != *accounts[3].key() { + return Err(ProgramError::InvalidSeeds); + } + + let account_infos = TransferToSplCpi { + source: &accounts[1], + destination_spl_token_account: &accounts[2], + amount: data.amount, + authority: &accounts[3], + mint: &accounts[4], + payer: &accounts[5], + spl_interface_pda: &accounts[6], + spl_interface_pda_bump: data.spl_interface_pda_bump, + decimals: data.decimals, + spl_token_program: &accounts[7], + compressed_token_program_authority: &accounts[8], + }; + + // Invoke with PDA signing + let authority_bump_byte = [authority_bump]; + let authority_seeds = [ + Seed::from(TRANSFER_AUTHORITY_SEED), + Seed::from(&authority_bump_byte[..]), + ]; + let authority_signer = Signer::from(&authority_seeds); + account_infos.invoke_signed(&[authority_signer])?; + + Ok(()) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs b/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs new file mode 100644 index 0000000000..a0a650659a --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/shared/mod.rs @@ -0,0 +1,476 @@ +// Shared test utilities for sdk-light-token-test + +use light_client::{indexer::Indexer, rpc::Rpc}; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; + +/// Program ID as Pubkey (converted from the [u8; 32] in the program) +#[allow(unused)] +pub const PROGRAM_ID: Pubkey = Pubkey::new_from_array(sdk_light_token_pinocchio_test::ID); + +/// Setup helper: Creates a compressed mint directly using the ctoken SDK (not via wrapper program) +/// Optionally creates ATAs and mints tokens for each recipient. +/// Note: This decompresses the mint first, then uses MintTo to mint to ctoken accounts. +/// Returns (mint_pda, compression_address, ata_pubkeys, mint_seed_keypair) +#[allow(unused)] +pub async fn setup_create_mint( + rpc: &mut (impl Rpc + Indexer), + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, + recipients: Vec<(u64, Pubkey)>, +) -> (Pubkey, [u8; 32], Vec, Keypair) { + use light_token::instruction::{ + CreateAssociatedTokenAccount, CreateMint, CreateMintParams, MintTo, + }; + + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + // Derive compression address using SDK helpers + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_seed.pubkey(), + &address_tree.tree, + ); + + let (mint, bump) = light_token::instruction::find_mint_address(&mint_seed.pubkey()); + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params for the SDK + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + // Create instruction directly using SDK + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + // Verify the compressed mint was created + let compressed_account = rpc + .get_compressed_account(compression_address, None) + .await + .unwrap() + .value; + + assert!( + compressed_account.is_some(), + "Compressed mint should exist after setup" + ); + + // If no recipients, return early + if recipients.is_empty() { + return (mint, compression_address, vec![], mint_seed); + } + + // Create ATAs for each recipient + use light_token::instruction::derive_token_ata; + + let mut ata_pubkeys = Vec::with_capacity(recipients.len()); + + for (_amount, owner) in &recipients { + let (ata_address, _bump) = derive_token_ata(owner, &mint); + ata_pubkeys.push(ata_address); + + let create_ata = CreateAssociatedTokenAccount::new(payer.pubkey(), *owner, mint); + let ata_instruction = create_ata.instruction().unwrap(); + + rpc.create_and_send_transaction(&[ata_instruction], &payer.pubkey(), &[payer]) + .await + .unwrap(); + } + + // Mint tokens to recipients with amount > 0 + let recipients_with_amount: Vec<_> = recipients + .iter() + .enumerate() + .filter(|(_, (amount, _))| *amount > 0) + .collect(); + + // Mint to each recipient using the decompressed Mint (CreateMint already decompresses) + for (idx, (amount, _)) in &recipients_with_amount { + let mint_instruction = MintTo { + mint, + destination: ata_pubkeys[*idx], + amount: *amount, + authority: mint_authority, + max_top_up: None, + fee_payer: None, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[mint_instruction], &payer.pubkey(), &[payer]) + .await + .unwrap(); + } + + (mint, compression_address, ata_pubkeys, mint_seed) +} + +/// Same as setup_create_mint but with optional freeze_authority +/// Returns (mint_pda, compression_address, ata_pubkeys) +#[allow(unused)] +pub async fn setup_create_mint_with_freeze_authority( + rpc: &mut (impl Rpc + Indexer), + payer: &Keypair, + mint_authority: Pubkey, + freeze_authority: Option, + decimals: u8, + recipients: Vec<(u64, Pubkey)>, +) -> (Pubkey, [u8; 32], Vec) { + use light_token::instruction::{ + CreateAssociatedTokenAccount, CreateMint, CreateMintParams, MintTo, + }; + + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + // Derive compression address using SDK helpers + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_seed.pubkey(), + &address_tree.tree, + ); + + let (mint, bump) = light_token::instruction::find_mint_address(&mint_seed.pubkey()); + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params for the SDK + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + // Create instruction directly using SDK + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + // Send transaction (CreateMint now creates both compressed mint AND Mint Solana account) + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + // If no recipients, return early + if recipients.is_empty() { + return (mint, compression_address, vec![]); + } + + // Create ATAs for each recipient + use light_token::instruction::derive_token_ata; + + let mut ata_pubkeys = Vec::with_capacity(recipients.len()); + + for (_amount, owner) in &recipients { + let (ata_address, _bump) = derive_token_ata(owner, &mint); + ata_pubkeys.push(ata_address); + + let create_ata = CreateAssociatedTokenAccount::new(payer.pubkey(), *owner, mint); + let ata_instruction = create_ata.instruction().unwrap(); + + rpc.create_and_send_transaction(&[ata_instruction], &payer.pubkey(), &[payer]) + .await + .unwrap(); + } + + // After decompression, use MintTo (simple 3-account instruction) + let recipients_with_amount: Vec<_> = recipients + .iter() + .enumerate() + .filter(|(_, (amount, _))| *amount > 0) + .collect(); + + if !recipients_with_amount.is_empty() { + for (idx, (amount, _)) in &recipients_with_amount { + let mint_instruction = MintTo { + mint, + destination: ata_pubkeys[*idx], + amount: *amount, + authority: mint_authority, + max_top_up: None, + fee_payer: None, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[mint_instruction], &payer.pubkey(), &[payer]) + .await + .unwrap(); + } + } + + (mint, compression_address, ata_pubkeys) +} + +/// Same as setup_create_mint but with compression_only flag set +#[allow(unused)] +pub async fn setup_create_mint_with_compression_only( + rpc: &mut (impl Rpc + Indexer), + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, + recipients: Vec<(u64, Pubkey)>, + compression_only: bool, +) -> (Pubkey, [u8; 32], Vec) { + use light_token::instruction::{ + CompressibleParams, CreateAssociatedTokenAccount, CreateMint, CreateMintParams, MintTo, + }; + + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + // Derive compression address using SDK helpers + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_seed.pubkey(), + &address_tree.tree, + ); + + let (mint, bump) = light_token::instruction::find_mint_address(&mint_seed.pubkey()); + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params for the SDK + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + // Create instruction directly using SDK + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + // Verify the compressed mint was created + let compressed_account = rpc + .get_compressed_account(compression_address, None) + .await + .unwrap() + .value; + + assert!( + compressed_account.is_some(), + "Compressed mint should exist after setup" + ); + + // If no recipients, return early + if recipients.is_empty() { + return (mint, compression_address, vec![]); + } + + // Create ATAs for each recipient with custom compression_only setting + use light_token::instruction::derive_token_ata; + + let mut ata_pubkeys = Vec::with_capacity(recipients.len()); + + // Build custom CompressibleParams with compression_only flag + let compressible_params = CompressibleParams { + compression_only, + ..Default::default() + }; + + for (_amount, owner) in &recipients { + let (ata_address, _bump) = derive_token_ata(owner, &mint); + ata_pubkeys.push(ata_address); + + let create_ata = CreateAssociatedTokenAccount::new(payer.pubkey(), *owner, mint) + .with_compressible(compressible_params.clone()); + let ata_instruction = create_ata.instruction().unwrap(); + + rpc.create_and_send_transaction(&[ata_instruction], &payer.pubkey(), &[payer]) + .await + .unwrap(); + } + + // Mint tokens to recipients with amount > 0 + let recipients_with_amount: Vec<_> = recipients + .iter() + .enumerate() + .filter(|(_, (amount, _))| *amount > 0) + .collect(); + + // Mint to each recipient using the decompressed Mint (CreateMint already decompresses) + for (idx, (amount, _)) in &recipients_with_amount { + let mint_instruction = MintTo { + mint, + destination: ata_pubkeys[*idx], + amount: *amount, + authority: mint_authority, + max_top_up: None, + fee_payer: None, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[mint_instruction], &payer.pubkey(), &[payer]) + .await + .unwrap(); + } + + (mint, compression_address, ata_pubkeys) +} + +/// Creates a compressed-only mint (no decompression) using light-token-client. +/// This creates ONLY the compressed mint account, NOT the Mint Solana account. +/// Use this to test the DecompressMint instruction. +/// Returns (mint_pda, compression_address, mint_seed_keypair) +#[allow(unused)] +pub async fn setup_create_compressed_only_mint( + rpc: &mut (impl Rpc + Indexer), + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, +) -> (Pubkey, [u8; 32], Keypair) { + use light_test_utils::actions::legacy::instructions::mint_action::{ + create_mint_action_instruction, MintActionParams, NewMint, + }; + use light_token::instruction::{derive_mint_compressed_address, find_mint_address}; + + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + + // Derive addresses + let compression_address = + derive_mint_compressed_address(&mint_seed.pubkey(), &address_tree.tree); + let (mint_pda, _bump) = find_mint_address(&mint_seed.pubkey()); + + // Create compressed-only mint using light-token-client + // By NOT including DecompressMint action, only the compressed mint is created + let create_ix = create_mint_action_instruction( + rpc, + MintActionParams { + compressed_mint_address: compression_address, + mint_seed: mint_seed.pubkey(), + authority: mint_authority, + payer: payer.pubkey(), + actions: vec![], // No actions - just create compressed mint + new_mint: Some(NewMint { + decimals, + supply: 0, + mint_authority, + freeze_authority: None, + metadata: None, + version: 3, + }), + }, + ) + .await + .unwrap(); + + // Send transaction - mint_seed must sign as mint_signer + rpc.create_and_send_transaction(&[create_ix], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + // Verify compressed mint was created + let compressed_account = rpc + .get_compressed_account(compression_address, None) + .await + .unwrap() + .value; + assert!( + compressed_account.is_some(), + "Compressed mint should exist after creation" + ); + + // Verify NO Mint Solana account exists + let mint_account = rpc.get_account(mint_pda).await.unwrap(); + assert!( + mint_account.is_none(), + "Mint Solana account should NOT exist for compressed-only mint" + ); + + (mint_pda, compression_address, mint_seed) +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs new file mode 100644 index 0000000000..1e22002a91 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_approve_revoke.rs @@ -0,0 +1,321 @@ +// Tests for ApproveCTokenCpi and RevokeCTokenCpi invoke() and invoke_signed() + +mod shared; + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::LIGHT_TOKEN_PROGRAM_ID; +use light_token_interface::state::Token; +use sdk_light_token_pinocchio_test::{ApproveData, InstructionType, TOKEN_ACCOUNT_SEED}; +use shared::*; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, +}; + +/// Test approving a delegate using ApproveCTokenCpi::invoke() +#[tokio::test] +async fn test_approve_invoke() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Create a compressed mint with an ATA for the payer with 1000 tokens + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(1000, payer.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + let delegate = Keypair::new(); + let approve_amount = 100u64; + + // Build approve instruction via wrapper program + let mut instruction_data = vec![InstructionType::ApproveInvoke as u8]; + let approve_data = ApproveData { + amount: approve_amount, + }; + approve_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_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 + ], + data: instruction_data, + }; + + // Execute the approve instruction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify the delegate was set + let ata_account = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken = Token::deserialize(&mut &ata_account.data[..]).unwrap(); + + assert_eq!( + ctoken.delegate, + Some(delegate.pubkey().to_bytes().into()), + "Delegate should be set after approve" + ); + assert_eq!( + ctoken.delegated_amount, approve_amount, + "Delegated amount should match" + ); +} + +/// Test approving a delegate for a PDA-owned account using ApproveCTokenCpi::invoke_signed() +#[tokio::test] +async fn test_approve_invoke_signed() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Derive the PDA that will own the token account + let (pda_owner, _bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &PROGRAM_ID); + + // Create a compressed mint with an ATA for the PDA owner with 1000 tokens + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = + setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9, vec![(1000, pda_owner)]).await; + + let ata = ata_pubkeys[0]; + let delegate = Keypair::new(); + let approve_amount = 100u64; + + // Build approve instruction via wrapper program using invoke_signed + let mut instruction_data = vec![InstructionType::ApproveInvokeSigned as u8]; + let approve_data = ApproveData { + amount: approve_amount, + }; + approve_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_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 + ], + data: instruction_data, + }; + + // Execute the approve instruction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify the delegate was set + let ata_account = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken = Token::deserialize(&mut &ata_account.data[..]).unwrap(); + + assert_eq!( + ctoken.delegate, + Some(delegate.pubkey().to_bytes().into()), + "Delegate should be set after approve" + ); + assert_eq!( + ctoken.delegated_amount, approve_amount, + "Delegated amount should match" + ); +} + +/// Test revoking delegation using RevokeCTokenCpi::invoke() +#[tokio::test] +async fn test_revoke_invoke() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Create a compressed mint with an ATA for the payer with 1000 tokens + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(1000, payer.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + let delegate = Keypair::new(); + let approve_amount = 100u64; + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + + // First approve a delegate + let mut approve_instruction_data = vec![InstructionType::ApproveInvoke as u8]; + let approve_data = ApproveData { + amount: approve_amount, + }; + approve_data + .serialize(&mut approve_instruction_data) + .unwrap(); + + let approve_instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(ata, false), + AccountMeta::new_readonly(delegate.pubkey(), false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(light_token_program, false), + ], + data: approve_instruction_data, + }; + + rpc.create_and_send_transaction(&[approve_instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify delegate was set + let ata_account_after_approve = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after_approve = + Token::deserialize(&mut &ata_account_after_approve.data[..]).unwrap(); + assert!( + ctoken_after_approve.delegate.is_some(), + "Delegate should be set" + ); + + // Now revoke the delegation + let revoke_instruction_data = vec![InstructionType::RevokeInvoke as u8]; + + let revoke_instruction = Instruction { + program_id: PROGRAM_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 + ], + data: revoke_instruction_data, + }; + + rpc.create_and_send_transaction(&[revoke_instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify the delegate was cleared + let ata_account_after_revoke = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after_revoke = Token::deserialize(&mut &ata_account_after_revoke.data[..]).unwrap(); + + assert_eq!( + ctoken_after_revoke.delegate, None, + "Delegate should be cleared after revoke" + ); + assert_eq!( + ctoken_after_revoke.delegated_amount, 0, + "Delegated amount should be 0 after revoke" + ); +} + +/// Test revoking delegation for a PDA-owned account using RevokeCTokenCpi::invoke_signed() +#[tokio::test] +async fn test_revoke_invoke_signed() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Derive the PDA that will own the token account + let (pda_owner, _bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &PROGRAM_ID); + + // Create a compressed mint with an ATA for the PDA owner with 1000 tokens + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = + setup_create_mint(&mut rpc, &payer, payer.pubkey(), 9, vec![(1000, pda_owner)]).await; + + let ata = ata_pubkeys[0]; + let delegate = Keypair::new(); + let approve_amount = 100u64; + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + + // First approve a delegate using invoke_signed + let mut approve_instruction_data = vec![InstructionType::ApproveInvokeSigned as u8]; + let approve_data = ApproveData { + amount: approve_amount, + }; + approve_data + .serialize(&mut approve_instruction_data) + .unwrap(); + + let approve_instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(ata, false), + AccountMeta::new_readonly(delegate.pubkey(), false), + AccountMeta::new(pda_owner, false), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(light_token_program, false), + ], + data: approve_instruction_data, + }; + + rpc.create_and_send_transaction(&[approve_instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify delegate was set + let ata_account_after_approve = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after_approve = + Token::deserialize(&mut &ata_account_after_approve.data[..]).unwrap(); + assert!( + ctoken_after_approve.delegate.is_some(), + "Delegate should be set" + ); + + // Now revoke the delegation using invoke_signed + let revoke_instruction_data = vec![InstructionType::RevokeInvokeSigned as u8]; + + let revoke_instruction = Instruction { + program_id: PROGRAM_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 + ], + data: revoke_instruction_data, + }; + + rpc.create_and_send_transaction(&[revoke_instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify the delegate was cleared + let ata_account_after_revoke = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after_revoke = Token::deserialize(&mut &ata_account_after_revoke.data[..]).unwrap(); + + assert_eq!( + ctoken_after_revoke.delegate, None, + "Delegate should be cleared after revoke" + ); + assert_eq!( + ctoken_after_revoke.delegated_amount, 0, + "Delegated amount should be 0 after revoke" + ); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs new file mode 100644 index 0000000000..ece1d29f34 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_burn.rs @@ -0,0 +1,151 @@ +// Tests for BurnCTokenCpi invoke() and invoke_signed() + +mod shared; + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::LIGHT_TOKEN_PROGRAM_ID; +use light_token_interface::state::Token; +use sdk_light_token_pinocchio_test::{BurnData, InstructionType, TOKEN_ACCOUNT_SEED}; +use shared::*; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signer::Signer, +}; + +/// Test burning CTokens using BurnCTokenCpi::invoke() +#[tokio::test] +async fn test_burn_invoke() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Create a decompressed mint (required for burn) with an ATA for the payer with 1000 tokens + let (mint_pda, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + None, // No freeze authority needed for burn test + 9, + vec![(1000, payer.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + let burn_amount = 300u64; + + // Get initial state + let ata_account_before = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_before = Token::deserialize(&mut &ata_account_before.data[..]).unwrap(); + + // Build burn instruction via wrapper program + let mut instruction_data = vec![InstructionType::BurnInvoke as u8]; + let burn_data = BurnData { + amount: burn_amount, + }; + burn_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(ata, false), // source + AccountMeta::new(mint_pda, false), // mint + AccountMeta::new(payer.pubkey(), true), // authority (writable, signer) + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + ], + data: instruction_data, + }; + + // Execute the burn instruction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify with single assert_eq + let ata_account_after = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after = Token::deserialize(&mut &ata_account_after.data[..]).unwrap(); + + let mut expected_ctoken = ctoken_before; + expected_ctoken.amount = 700; // 1000 - 300 + + assert_eq!( + ctoken_after, expected_ctoken, + "Light Token should match expected state after burn" + ); +} + +/// Test burning CTokens with PDA authority using BurnCTokenCpi::invoke_signed() +#[tokio::test] +async fn test_burn_invoke_signed() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Derive the PDA that will own the token account + let (pda_owner, _bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &PROGRAM_ID); + + // Create a decompressed mint with an ATA for the PDA owner with 1000 tokens + let (mint_pda, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + None, // No freeze authority needed for burn test + 9, + vec![(1000, pda_owner)], + ) + .await; + + let ata = ata_pubkeys[0]; + let burn_amount = 500u64; + + // Get initial state + let ata_account_before = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_before = Token::deserialize(&mut &ata_account_before.data[..]).unwrap(); + + // Build burn instruction via wrapper program using invoke_signed + let mut instruction_data = vec![InstructionType::BurnInvokeSigned as u8]; + let burn_data = BurnData { + amount: burn_amount, + }; + burn_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(ata, false), // source + AccountMeta::new(mint_pda, false), // mint + AccountMeta::new(pda_owner, false), // PDA authority (writable, program signs) + AccountMeta::new_readonly(light_token_program, false), // light_token_program + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + ], + data: instruction_data, + }; + + // Execute the burn instruction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify with single assert_eq + let ata_account_after = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after = Token::deserialize(&mut &ata_account_after.data[..]).unwrap(); + + let mut expected_ctoken = ctoken_before; + expected_ctoken.amount = 500; // 1000 - 500 + + assert_eq!( + ctoken_after, expected_ctoken, + "Light Token should match expected state after burn" + ); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_close.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_close.rs new file mode 100644 index 0000000000..87760eeb98 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_close.rs @@ -0,0 +1,131 @@ +// Tests for CloseCTokenAccountCpi invoke() and invoke_signed() + +mod shared; + +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::instruction::{rent_sponsor_pda, LIGHT_TOKEN_PROGRAM_ID}; +use sdk_light_token_pinocchio_test::{InstructionType, TOKEN_ACCOUNT_SEED}; +use shared::*; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signer::Signer, +}; + +/// Test closing a compressible token account using CloseCTokenAccountCpi::invoke() +#[tokio::test] +async fn test_close_invoke() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Create a compressed mint with an ATA for the payer + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(0, payer.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + + // Verify the ATA exists + let ata_account = rpc.get_account(ata).await.unwrap(); + assert!(ata_account.is_some(), "ATA should exist before close"); + + // Get rent sponsor + let rent_sponsor = rent_sponsor_pda(); + + // Build instruction to close via wrapper program + let instruction_data = vec![InstructionType::CloseAccountInvoke as u8]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), // token_program + AccountMeta::new(ata, false), // account to close + AccountMeta::new(payer.pubkey(), false), // destination + AccountMeta::new(payer.pubkey(), true), // owner (signer) + AccountMeta::new(rent_sponsor, false), // rent_sponsor + ], + data: instruction_data, + }; + + // Execute the close instruction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify the ATA is closed + let ata_account_after = rpc.get_account(ata).await.unwrap(); + assert!( + ata_account_after.is_none(), + "ATA should be closed after close instruction" + ); +} + +/// Test closing a PDA-owned compressible token account using CloseCTokenAccountCpi::invoke_signed() +#[tokio::test] +async fn test_close_invoke_signed() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Derive the PDA that will own the token account + let (pda_owner, _bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &PROGRAM_ID); + + // Create a compressed mint with an ATA for the PDA owner + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(0, pda_owner)], // PDA will own this ATA + ) + .await; + + let ata = ata_pubkeys[0]; + + // Verify the ATA exists + let ata_account = rpc.get_account(ata).await.unwrap(); + assert!(ata_account.is_some(), "ATA should exist before close"); + + // Get rent sponsor + let rent_sponsor = rent_sponsor_pda(); + + // Build instruction to close via wrapper program using invoke_signed + let instruction_data = vec![InstructionType::CloseAccountInvokeSigned as u8]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), // token_program + AccountMeta::new(ata, false), // account to close + AccountMeta::new(payer.pubkey(), false), // destination + AccountMeta::new(pda_owner, false), // owner (PDA, mutable for write_top_up) + AccountMeta::new(rent_sponsor, false), // rent_sponsor + ], + data: instruction_data, + }; + + // Execute the close instruction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify the ATA is closed + let ata_account_after = rpc.get_account(ata).await.unwrap(); + assert!( + ata_account_after.is_none(), + "PDA-owned ATA should be closed after close instruction" + ); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_create_ata.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_create_ata.rs new file mode 100644 index 0000000000..3787f3d133 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_create_ata.rs @@ -0,0 +1,175 @@ +// Tests for CreateAssociatedTokenAccountCpi (CreateAta instructions) + +mod shared; + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; +use sdk_light_token_pinocchio_test::{CreateAtaData, ATA_SEED}; +use shared::*; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signer::Signer, +}; + +/// Test creating an ATA using CreateAssociatedTokenAccountCpi::invoke() +#[tokio::test] +async fn test_create_ata_invoke() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let mint_authority = payer.pubkey(); + + // Create compressed mint first (using helper) + let (mint_pda, _compression_address, _, _mint_seed) = + setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; + + // Derive the ATA address + let owner = payer.pubkey(); + use light_token::instruction::derive_token_ata; + let (ata_address, bump) = derive_token_ata(&owner, &mint_pda); + + // Build CreateAtaData (owner and mint are passed as accounts) + let create_ata_data = CreateAtaData { + bump, + pre_pay_num_epochs: 2, + lamports_per_write: 1, + }; + // Discriminator 4 = CreateAtaInvoke + let instruction_data = [vec![4u8], create_ata_data.try_to_vec().unwrap()].concat(); + + use light_token::instruction::{config_pda, rent_sponsor_pda}; + let config = config_pda(); + let rent_sponsor = rent_sponsor_pda(); + + // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, light_token_program + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(owner, false), + AccountMeta::new_readonly(mint_pda, false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(ata_address, false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(config, false), + AccountMeta::new(rent_sponsor, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify ATA was created + let ata_account_data = rpc.get_account(ata_address).await.unwrap().unwrap(); + + // Parse and verify account data + use light_token_interface::state::Token; + let account_state = Token::deserialize(&mut &ata_account_data.data[..]).unwrap(); + assert_eq!( + account_state.mint.to_bytes(), + mint_pda.to_bytes(), + "Mint should match" + ); + assert_eq!( + account_state.owner.to_bytes(), + owner.to_bytes(), + "Owner should match" + ); + assert_eq!(account_state.amount, 0, "Initial amount should be 0"); +} + +/// Test creating an ATA with PDA payer using CreateAssociatedTokenAccountCpi::invoke_signed() +#[tokio::test] +async fn test_create_ata_invoke_signed() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let mint_authority = payer.pubkey(); + + // Create compressed mint first (using helper) + let (mint_pda, _compression_address, _, _mint_seed) = + setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; + + // Derive the PDA that will act as payer/owner (using ATA_SEED) + let (pda_owner, _pda_bump) = Pubkey::find_program_address(&[ATA_SEED], &PROGRAM_ID); + + // Fund the PDA so it can pay for the ATA creation + let fund_ix = solana_sdk::system_instruction::transfer( + &payer.pubkey(), + &pda_owner, + 1_000_000_000, // 1 SOL + ); + rpc.create_and_send_transaction(&[fund_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Derive the ATA address for the PDA owner + use light_token::instruction::derive_token_ata; + let (ata_address, bump) = derive_token_ata(&pda_owner, &mint_pda); + + // Build CreateAtaData with PDA as owner (owner and mint are passed as accounts) + let create_ata_data = CreateAtaData { + bump, + pre_pay_num_epochs: 2, + lamports_per_write: 1, + }; + // Discriminator 5 = CreateAtaInvokeSigned + let instruction_data = [vec![5u8], create_ata_data.try_to_vec().unwrap()].concat(); + + use light_token::instruction::{config_pda, rent_sponsor_pda}; + let config = config_pda(); + let rent_sponsor = rent_sponsor_pda(); + + // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, light_token_program + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(pda_owner, false), // owner + AccountMeta::new_readonly(mint_pda, false), + AccountMeta::new(pda_owner, false), // PDA payer - not a signer (program signs via invoke_signed) + AccountMeta::new(ata_address, false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(config, false), + AccountMeta::new(rent_sponsor, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify ATA was created + let ata_account_data = rpc.get_account(ata_address).await.unwrap().unwrap(); + + // Parse and verify account data + use light_token_interface::state::Token; + let account_state = Token::deserialize(&mut &ata_account_data.data[..]).unwrap(); + assert_eq!( + account_state.mint.to_bytes(), + mint_pda.to_bytes(), + "Mint should match" + ); + assert_eq!( + account_state.owner.to_bytes(), + pda_owner.to_bytes(), + "Owner should match PDA" + ); + assert_eq!(account_state.amount, 0, "Initial amount should be 0"); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_create_mint.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_create_mint.rs new file mode 100644 index 0000000000..df4203d5dd --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_create_mint.rs @@ -0,0 +1,251 @@ +// Tests for CreateMintCpi (CreateCmint instruction) + +mod shared; + +use borsh::BorshSerialize; +use light_client::{indexer::Indexer, rpc::Rpc}; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::{ + constants::{config_pda, rent_sponsor_pda}, + instruction::{CreateMint, CreateMintParams}, +}; +use light_token_interface::{ + instructions::extensions::{ + token_metadata::TokenMetadataInstructionData, ExtensionInstructionData, + }, + state::AdditionalMetadata, +}; +use sdk_light_token_pinocchio_test::{CreateCmintData, MINT_SIGNER_SEED}; +use shared::PROGRAM_ID; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, +}; + +/// Test creating a compressed mint using CreateMintCpi::invoke() +#[tokio::test] +async fn test_create_compressed_mint() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let mint_signer = Keypair::new(); + let decimals = 9u8; + let mint_authority = payer.pubkey(); + + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + + // Use SDK helper to derive the compression address correctly + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_signer.pubkey(), + &address_tree.tree, + ); + + let (mint_pda, mint_bump) = light_token::instruction::find_mint_address(&mint_signer.pubkey()); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params for CreateMint (used for both instruction data and account metas) + let proof = rpc_result.proof.0.unwrap(); + let extensions = Some(vec![ExtensionInstructionData::TokenMetadata( + TokenMetadataInstructionData { + update_authority: Some(payer.pubkey().to_bytes().into()), + name: b"Test Token".to_vec(), + symbol: b"TEST".to_vec(), + uri: b"https://example.com/metadata.json".to_vec(), + additional_metadata: Some(vec![ + AdditionalMetadata { + key: b"test1".to_vec(), + value: b"value1".to_vec(), + }, + AdditionalMetadata { + key: b"test2".to_vec(), + value: b"value2".to_vec(), + }, + ]), + }, + )]); + + let create_mint_params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof, + compression_address, + mint: mint_pda, + bump: mint_bump, + freeze_authority: None, + extensions: extensions.clone(), + rent_payment: 16, + write_top_up: 766, + }; + + // Create instruction data for wrapper program + let create_mint_data = CreateCmintData { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority: mint_authority.to_bytes(), + proof, + compression_address, + mint: mint_pda.to_bytes(), + bump: mint_bump, + freeze_authority: None, + extensions, + rent_payment: 16, + write_top_up: 766, + }; + let instruction_data = [vec![0u8], create_mint_data.try_to_vec().unwrap()].concat(); + + // Use CreateMint builder to get the correct account metas + let create_mint_ix = CreateMint::new( + create_mint_params, + mint_signer.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ) + .instruction() + .unwrap(); + + // Add compressed token program as first account for CPI, then all SDK-generated accounts + let mut wrapper_accounts = vec![AccountMeta::new_readonly( + compressed_token_program_id, + false, + )]; + wrapper_accounts.extend(create_mint_ix.accounts); + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &mint_signer]) + .await + .unwrap(); + + // Verify the Mint Solana account was created (CreateMint now decompresses automatically) + let mint_account = rpc.get_account(mint_pda).await.unwrap(); + assert!(mint_account.is_some(), "Mint Solana account should exist"); +} + +/// Test creating a compressed mint with PDA mint signer using CreateMintCpi::invoke_signed() +#[tokio::test] +async fn test_create_compressed_mint_invoke_signed() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let decimals = 9u8; + let mint_authority = payer.pubkey(); + + // Derive the PDA mint signer from our program + let (mint_signer_pda, _bump) = Pubkey::find_program_address(&[MINT_SIGNER_SEED], &PROGRAM_ID); + + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + + // Use SDK helper to derive the compression address correctly + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_signer_pda, + &address_tree.tree, + ); + + let (mint_pda, mint_bump) = light_token::instruction::find_mint_address(&mint_signer_pda); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Create instruction data for wrapper program + let create_mint_data = CreateCmintData { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority: mint_authority.to_bytes(), + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint: mint_pda.to_bytes(), + bump: mint_bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + // Discriminator 12 = CreateCmintInvokeSigned + let instruction_data = [vec![12u8], create_mint_data.try_to_vec().unwrap()].concat(); + + // Build accounts manually since SDK marks mint_signer as signer, but we need it as non-signer + // for invoke_signed (the wrapper program signs via CPI) + // Account order matches MintActionMetaConfig::to_account_metas() with mint_signer as non-signer + let system_accounts = light_token::instruction::SystemAccounts::default(); + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), // [0] compressed_token_program + AccountMeta::new_readonly(system_accounts.light_system_program, false), // [1] light_system_program + // mint_signer NOT marked as signer - program will sign via invoke_signed + AccountMeta::new_readonly(mint_signer_pda, false), // [2] mint_signer (PDA) + AccountMeta::new_readonly(payer.pubkey(), true), // [3] authority (signer) + AccountMeta::new_readonly(config_pda(), false), // [4] compressible_config + AccountMeta::new(mint_pda, false), // [5] mint + AccountMeta::new(rent_sponsor_pda(), false), // [6] rent_sponsor + AccountMeta::new(payer.pubkey(), true), // [7] fee_payer (signer) + AccountMeta::new_readonly(system_accounts.cpi_authority_pda, false), // [8] + AccountMeta::new_readonly(system_accounts.registered_program_pda, false), // [9] + AccountMeta::new_readonly(system_accounts.account_compression_authority, false), // [10] + AccountMeta::new_readonly(system_accounts.account_compression_program, false), // [11] + AccountMeta::new_readonly(system_accounts.system_program, false), // [12] + AccountMeta::new(output_queue, false), // [13] + AccountMeta::new(address_tree.tree, false), // [14] + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: instruction_data, + }; + + // Note: only payer signs, the mint_signer PDA is signed by the program via invoke_signed + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify the Mint Solana account was created (CreateMint now decompresses automatically) + let mint_account = rpc.get_account(mint_pda).await.unwrap(); + assert!(mint_account.is_some(), "Mint Solana account should exist"); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_create_token_account.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_create_token_account.rs new file mode 100644 index 0000000000..2bd383f880 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_create_token_account.rs @@ -0,0 +1,163 @@ +// Tests for CreateTokenAccountCpi (CreateTokenAccount instructions) + +mod shared; + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; +use sdk_light_token_pinocchio_test::CreateTokenAccountData; +use shared::*; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, +}; + +/// Test creating a token account using CreateTokenAccountCpi::invoke() +#[tokio::test] +async fn test_create_token_account_invoke() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let mint_authority = payer.pubkey(); + + // Create compressed mint first (using helper) + let (mint_pda, _compression_address, _, _mint_seed) = + setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; + + // Create ctoken account via wrapper program + let ctoken_account = Keypair::new(); + let owner = payer.pubkey(); + + let create_token_account_data = CreateTokenAccountData { + owner: owner.to_bytes(), + pre_pay_num_epochs: 2, + lamports_per_write: 1, + }; + let instruction_data = [vec![2u8], create_token_account_data.try_to_vec().unwrap()].concat(); + + use light_token::instruction::{config_pda, rent_sponsor_pda}; + let config = config_pda(); + let rent_sponsor = rent_sponsor_pda(); + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(ctoken_account.pubkey(), true), + AccountMeta::new_readonly(mint_pda, false), + AccountMeta::new_readonly(config, false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(rent_sponsor, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &ctoken_account]) + .await + .unwrap(); + + // Verify ctoken account was created + let ctoken_account_data = rpc + .get_account(ctoken_account.pubkey()) + .await + .unwrap() + .unwrap(); + + // Parse and verify account data + use light_token_interface::state::Token; + let account_state = Token::deserialize(&mut &ctoken_account_data.data[..]).unwrap(); + assert_eq!( + account_state.mint.to_bytes(), + mint_pda.to_bytes(), + "Mint should match" + ); + assert_eq!( + account_state.owner.to_bytes(), + owner.to_bytes(), + "Owner should match" + ); + assert_eq!(account_state.amount, 0, "Initial amount should be 0"); +} + +/// Test creating a PDA-owned token account using CreateTokenAccountCpi::invoke_signed() +#[tokio::test] +async fn test_create_token_account_invoke_signed() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let mint_authority = payer.pubkey(); + + // Create compressed mint first (using helper) + let (mint_pda, _compression_address, _, _mint_seed) = + setup_create_mint(&mut rpc, &payer, mint_authority, 9, vec![]).await; + + // Derive the PDA for the token account (same seeds as in the program) + let token_account_seed: &[u8] = b"token_account"; + let (ctoken_account_pda, _bump) = + Pubkey::find_program_address(&[token_account_seed], &PROGRAM_ID); + + let owner = payer.pubkey(); + + let create_token_account_data = CreateTokenAccountData { + owner: owner.to_bytes(), + pre_pay_num_epochs: 2, + lamports_per_write: 1, + }; + // Discriminator 3 = CreateTokenAccountInvokeSigned + let instruction_data = [vec![3u8], create_token_account_data.try_to_vec().unwrap()].concat(); + + use light_token::instruction::{config_pda, rent_sponsor_pda}; + let config = config_pda(); + let rent_sponsor = rent_sponsor_pda(); + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(ctoken_account_pda, false), // PDA, not a signer + AccountMeta::new_readonly(mint_pda, false), + AccountMeta::new_readonly(config, false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new(rent_sponsor, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + // Note: only payer signs, the PDA account is signed by the program via invoke_signed + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify ctoken account was created + let ctoken_account_data = rpc.get_account(ctoken_account_pda).await.unwrap().unwrap(); + + // Parse and verify account data + use light_token_interface::state::Token; + let account_state = Token::deserialize(&mut &ctoken_account_data.data[..]).unwrap(); + assert_eq!( + account_state.mint.to_bytes(), + mint_pda.to_bytes(), + "Mint should match" + ); + assert_eq!( + account_state.owner.to_bytes(), + owner.to_bytes(), + "Owner should match" + ); + assert_eq!(account_state.amount, 0, "Initial amount should be 0"); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs new file mode 100644 index 0000000000..11458e67ea --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_ctoken_mint_to.rs @@ -0,0 +1,260 @@ +// Tests for CTokenMintToCpi invoke() and invoke_signed() + +mod shared; + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::LIGHT_TOKEN_PROGRAM_ID; +use light_token_interface::state::Token; +use sdk_light_token_pinocchio_test::{InstructionType, MintToData, MINT_AUTHORITY_SEED}; +use shared::*; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signer::Signer, +}; + +/// Test minting to Light Token using CTokenMintToCpi::invoke() +#[tokio::test] +async fn test_ctoken_mint_to_invoke() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Create a decompressed mint with an ATA for the payer with 0 tokens + let (mint_pda, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), // mint authority is payer + None, + 9, + vec![(0, payer.pubkey())], // Start with 0 tokens + ) + .await; + + let ata = ata_pubkeys[0]; + let mint_amount = 500u64; + + // Get initial state + let ata_account_before = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_before = Token::deserialize(&mut &ata_account_before.data[..]).unwrap(); + + // Build mint instruction via wrapper program + let mut instruction_data = vec![InstructionType::CTokenMintToInvoke as u8]; + let mint_data = MintToData { + amount: mint_amount, + }; + mint_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let system_program = Pubkey::default(); + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(mint_pda, false), // mint + 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 + ], + data: instruction_data, + }; + + // Execute the mint instruction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify with single assert_eq + let ata_account_after = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after = Token::deserialize(&mut &ata_account_after.data[..]).unwrap(); + + let mut expected_ctoken = ctoken_before; + expected_ctoken.amount = 500; // 0 + 500 + + assert_eq!( + ctoken_after, expected_ctoken, + "Light Token should match expected state after mint" + ); +} + +/// Test minting to Light Token with PDA authority using CTokenMintToCpi::invoke_signed() +/// +/// This test: +/// 1. Creates a compressed mint with PDA authority via wrapper program (auto-decompresses) +/// 2. Creates an ATA +/// 3. Mints tokens using PDA authority via invoke_signed +#[tokio::test] +async fn test_ctoken_mint_to_invoke_signed() { + use light_client::indexer::Indexer; + use light_token::instruction::CreateAssociatedTokenAccount; + use sdk_light_token_pinocchio_test::{CreateCmintData, MINT_SIGNER_SEED}; + + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Derive the PDAs from our wrapper program + let (mint_signer_pda, _) = Pubkey::find_program_address(&[MINT_SIGNER_SEED], &PROGRAM_ID); + let (pda_mint_authority, _) = Pubkey::find_program_address(&[MINT_AUTHORITY_SEED], &PROGRAM_ID); + + let decimals = 9u8; + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + // Derive compression address using the PDA mint_signer + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_signer_pda, + &address_tree.tree, + ); + + let (mint_pda, mint_bump) = light_token::instruction::find_mint_address(&mint_signer_pda); + + // Step 1: Create compressed mint with PDA authority using wrapper program (discriminator 14) + { + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let default_pubkeys = light_token::utils::TokenDefaultAccounts::default(); + let compressible_config = light_token::instruction::config_pda(); + let rent_sponsor = light_token::instruction::rent_sponsor_pda(); + + let create_mint_data = CreateCmintData { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority: pda_mint_authority.to_bytes(), + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint: mint_pda.to_bytes(), + bump: mint_bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + // Discriminator 14 = CreateCmintWithPdaAuthority + let wrapper_instruction_data = + [vec![14u8], create_mint_data.try_to_vec().unwrap()].concat(); + + // Account order matches process_create_mint_with_pda_authority (MintActionMetaConfig): + // [0]: compressed_token_program + // [1]: light_system_program + // [2]: mint_signer (PDA) + // [3]: authority (PDA) + // [4]: compressible_config + // [5]: mint + // [6]: rent_sponsor + // [7]: fee_payer (signer) + // [8]: cpi_authority_pda + // [9]: registered_program_pda + // [10]: account_compression_authority + // [11]: account_compression_program + // [12]: system_program + // [13]: output_queue + // [14]: address_tree + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), // [0] + AccountMeta::new_readonly(default_pubkeys.light_system_program, false), // [1] + AccountMeta::new_readonly(mint_signer_pda, false), // [2] mint_signer PDA + AccountMeta::new_readonly(pda_mint_authority, false), // [3] authority PDA + AccountMeta::new_readonly(compressible_config, false), // [4] compressible_config + AccountMeta::new(mint_pda, false), // [5] mint + AccountMeta::new(rent_sponsor, false), // [6] rent_sponsor + AccountMeta::new(payer.pubkey(), true), // [7] fee_payer (signer) + AccountMeta::new_readonly(default_pubkeys.cpi_authority_pda, false), // [8] + AccountMeta::new_readonly(default_pubkeys.registered_program_pda, false), // [9] + AccountMeta::new_readonly(default_pubkeys.account_compression_authority, false), // [10] + AccountMeta::new_readonly(default_pubkeys.account_compression_program, false), // [11] + AccountMeta::new_readonly(default_pubkeys.system_program, false), // [12] + AccountMeta::new(output_queue, false), // [13] + AccountMeta::new(address_tree.tree, false), // [14] + ]; + + let create_mint_ix = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[create_mint_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + } + + // Step 2: Create ATA for payer (CreateMint now auto-decompresses) + let ata = { + let (ata_address, _) = + light_token::instruction::derive_token_ata(&payer.pubkey(), &mint_pda); + let create_ata = + CreateAssociatedTokenAccount::new(payer.pubkey(), payer.pubkey(), mint_pda); + let ata_instruction = create_ata.instruction().unwrap(); + + rpc.create_and_send_transaction(&[ata_instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + ata_address + }; + + let mint_amount = 1000u64; + + // Get initial state + let ata_account_before = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_before = Token::deserialize(&mut &ata_account_before.data[..]).unwrap(); + + // Step 3: Mint tokens using PDA authority via invoke_signed + let mut instruction_data = vec![InstructionType::CTokenMintToInvokeSigned as u8]; + let mint_data = MintToData { + amount: mint_amount, + }; + mint_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let system_program = Pubkey::default(); + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(mint_pda, false), // mint + 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 + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify with single assert_eq + let ata_account_after = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after = Token::deserialize(&mut &ata_account_after.data[..]).unwrap(); + + let mut expected_ctoken = ctoken_before; + expected_ctoken.amount = 1000; // 0 + 1000 + + assert_eq!( + ctoken_after, expected_ctoken, + "Light Token should match expected state after mint" + ); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_freeze_thaw.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_freeze_thaw.rs new file mode 100644 index 0000000000..845f489710 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_freeze_thaw.rs @@ -0,0 +1,314 @@ +// Tests for FreezeCTokenCpi and ThawCTokenCpi invoke() and invoke_signed() + +mod shared; + +use borsh::BorshDeserialize; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::LIGHT_TOKEN_PROGRAM_ID; +use light_token_interface::state::{AccountState, Token}; +use sdk_light_token_pinocchio_test::{InstructionType, FREEZE_AUTHORITY_SEED}; +use shared::*; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, +}; + +/// Test freezing a Light Token account using FreezeCTokenCpi::invoke() +#[tokio::test] +async fn test_freeze_invoke() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let freeze_authority = Keypair::new(); + + // Create a compressed mint with freeze_authority and an ATA for the payer with 1000 tokens + let (mint_pda, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + Some(freeze_authority.pubkey()), + 9, + vec![(1000, payer.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + + // Verify account is initially unfrozen + let ata_account_before = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_before = Token::deserialize(&mut &ata_account_before.data[..]).unwrap(); + assert_eq!( + ctoken_before.state, + AccountState::Initialized, + "Account should be initialized (unfrozen) before freeze" + ); + + // Build freeze instruction via wrapper program + let instruction_data = vec![InstructionType::FreezeInvoke as u8]; + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_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(light_token_program, false), // light_token_program + ], + data: instruction_data, + }; + + // Execute the freeze instruction + rpc.create_and_send_transaction( + &[instruction], + &payer.pubkey(), + &[&payer, &freeze_authority], + ) + .await + .unwrap(); + + // Verify the account is now frozen + let ata_account_after = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after = Token::deserialize(&mut &ata_account_after.data[..]).unwrap(); + + assert_eq!( + ctoken_after.state, + AccountState::Frozen, + "Account should be frozen after freeze" + ); +} + +/// Test freezing a Light Token account with PDA freeze authority using FreezeCTokenCpi::invoke_signed() +#[tokio::test] +async fn test_freeze_invoke_signed() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Derive the PDA that will be the freeze authority + let (pda_freeze_authority, _bump) = + Pubkey::find_program_address(&[FREEZE_AUTHORITY_SEED], &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) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + Some(pda_freeze_authority), + 9, + vec![(1000, payer.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + + // Build freeze instruction via wrapper program using invoke_signed + let instruction_data = vec![InstructionType::FreezeInvokeSigned as u8]; + + let light_token_program = LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_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(light_token_program, false), // light_token_program + ], + data: instruction_data, + }; + + // Execute the freeze instruction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify the account is now frozen + let ata_account_after = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after = Token::deserialize(&mut &ata_account_after.data[..]).unwrap(); + + assert_eq!( + ctoken_after.state, + AccountState::Frozen, + "Account should be frozen after freeze" + ); +} + +/// Test thawing a frozen Light Token account using ThawCTokenCpi::invoke() +#[tokio::test] +async fn test_thaw_invoke() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let freeze_authority = Keypair::new(); + let light_token_program = 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) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + Some(freeze_authority.pubkey()), + 9, + vec![(1000, payer.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + + // First freeze the account + let freeze_instruction_data = vec![InstructionType::FreezeInvoke as u8]; + let freeze_instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(ata, false), + AccountMeta::new_readonly(mint_pda, false), + AccountMeta::new_readonly(freeze_authority.pubkey(), true), + AccountMeta::new_readonly(light_token_program, false), + ], + data: freeze_instruction_data, + }; + + rpc.create_and_send_transaction( + &[freeze_instruction], + &payer.pubkey(), + &[&payer, &freeze_authority], + ) + .await + .unwrap(); + + // Verify account is frozen + let ata_account_after_freeze = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after_freeze = Token::deserialize(&mut &ata_account_after_freeze.data[..]).unwrap(); + assert_eq!( + ctoken_after_freeze.state, + AccountState::Frozen, + "Account should be frozen" + ); + + // Now thaw the account + let thaw_instruction_data = vec![InstructionType::ThawInvoke as u8]; + let thaw_instruction = Instruction { + program_id: PROGRAM_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(light_token_program, false), // light_token_program + ], + data: thaw_instruction_data, + }; + + rpc.create_and_send_transaction( + &[thaw_instruction], + &payer.pubkey(), + &[&payer, &freeze_authority], + ) + .await + .unwrap(); + + // Verify the account is now thawed (initialized) + let ata_account_after_thaw = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after_thaw = Token::deserialize(&mut &ata_account_after_thaw.data[..]).unwrap(); + + assert_eq!( + ctoken_after_thaw.state, + AccountState::Initialized, + "Account should be initialized (thawed) after thaw" + ); +} + +/// Test thawing a frozen Light Token account with PDA freeze authority using ThawCTokenCpi::invoke_signed() +#[tokio::test] +async fn test_thaw_invoke_signed() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Derive the PDA that will be the freeze authority + let (pda_freeze_authority, _bump) = + Pubkey::find_program_address(&[FREEZE_AUTHORITY_SEED], &PROGRAM_ID); + let light_token_program = 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) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + Some(pda_freeze_authority), + 9, + vec![(1000, payer.pubkey())], + ) + .await; + + let ata = ata_pubkeys[0]; + + // First freeze the account using invoke_signed + let freeze_instruction_data = vec![InstructionType::FreezeInvokeSigned as u8]; + let freeze_instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(ata, false), + AccountMeta::new_readonly(mint_pda, false), + AccountMeta::new_readonly(pda_freeze_authority, false), + AccountMeta::new_readonly(light_token_program, false), + ], + data: freeze_instruction_data, + }; + + rpc.create_and_send_transaction(&[freeze_instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify account is frozen + let ata_account_after_freeze = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after_freeze = Token::deserialize(&mut &ata_account_after_freeze.data[..]).unwrap(); + assert_eq!( + ctoken_after_freeze.state, + AccountState::Frozen, + "Account should be frozen" + ); + + // Now thaw the account using invoke_signed + let thaw_instruction_data = vec![InstructionType::ThawInvokeSigned as u8]; + let thaw_instruction = Instruction { + program_id: PROGRAM_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(light_token_program, false), // light_token_program + ], + data: thaw_instruction_data, + }; + + rpc.create_and_send_transaction(&[thaw_instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify the account is now thawed (initialized) + let ata_account_after_thaw = rpc.get_account(ata).await.unwrap().unwrap(); + let ctoken_after_thaw = Token::deserialize(&mut &ata_account_after_thaw.data[..]).unwrap(); + + assert_eq!( + ctoken_after_thaw.state, + AccountState::Initialized, + "Account should be initialized (thawed) after thaw" + ); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs new file mode 100644 index 0000000000..4f8bc73872 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer.rs @@ -0,0 +1,136 @@ +// Tests for CTokenTransfer invoke() and invoke_signed() + +mod shared; + +use borsh::{BorshDeserialize, BorshSerialize}; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; +use sdk_light_token_pinocchio_test::{InstructionType, TransferData, TOKEN_ACCOUNT_SEED}; +use shared::*; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signer::Signer, +}; + +/// Test CTokenTransfer using invoke() +#[tokio::test] +async fn test_ctoken_transfer_invoke() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let source_owner = payer.pubkey(); + let dest_owner = Pubkey::new_unique(); + + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(1000, source_owner), (0, dest_owner)], + ) + .await; + + let source_ata = ata_pubkeys[0]; + let dest_ata = ata_pubkeys[1]; + + // Transfer 500 tokens + let transfer_data = TransferData { amount: 500 }; + let instruction_data = [ + vec![InstructionType::CTokenTransferInvoke as u8], + transfer_data.try_to_vec().unwrap(), + ] + .concat(); + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new(source_owner, true), // authority (writable, signer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify final balances + use light_token_interface::state::Token; + let source_data_after = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state_after = Token::deserialize(&mut &source_data_after.data[..]).unwrap(); + assert_eq!(source_state_after.amount, 500); + + let dest_data_after = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state_after = Token::deserialize(&mut &dest_data_after.data[..]).unwrap(); + assert_eq!(dest_state_after.amount, 500); +} + +/// Test CTokenTransfer using invoke_signed() with PDA authority +#[tokio::test] +async fn test_ctoken_transfer_invoke_signed() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + // Derive the PDA that will own the source account + let (pda_owner, _bump) = Pubkey::find_program_address(&[TOKEN_ACCOUNT_SEED], &PROGRAM_ID); + let dest_owner = payer.pubkey(); + + let (_mint_pda, _compression_address, ata_pubkeys, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), + 9, + vec![(1000, pda_owner), (0, dest_owner)], + ) + .await; + + let source_ata = ata_pubkeys[0]; + let dest_ata = ata_pubkeys[1]; + + // Transfer 300 tokens using invoke_signed + let transfer_data = TransferData { amount: 300 }; + let instruction_data = [ + vec![InstructionType::CTokenTransferInvokeSigned as u8], + transfer_data.try_to_vec().unwrap(), + ] + .concat(); + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new(pda_owner, false), // PDA authority (writable, program signs) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify final balances + use light_token_interface::state::Token; + let source_data_after = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state_after = Token::deserialize(&mut &source_data_after.data[..]).unwrap(); + assert_eq!(source_state_after.amount, 700); + + let dest_data_after = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state_after = Token::deserialize(&mut &dest_data_after.data[..]).unwrap(); + assert_eq!(dest_state_after.amount, 300); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs new file mode 100644 index 0000000000..8f65c60888 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_checked.rs @@ -0,0 +1,349 @@ +// Tests for TransferCTokenCheckedCpi with different mint types + +mod shared; +use anchor_spl::token::{spl_token, Mint}; +use borsh::{BorshDeserialize, BorshSerialize}; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_test_utils::{ + mint_2022::{create_mint_22_with_extensions, create_token_22_account, mint_spl_tokens_22}, + spl::{create_token_account, mint_spl_tokens}, +}; +use light_token::{ + instruction::{derive_token_ata, CreateAssociatedTokenAccount, TransferFromSpl}, + spl_interface::{find_spl_interface_pda_with_index, CreateSplInterfacePda}, +}; +use light_token_interface::state::Token; +use sdk_light_token_pinocchio_test::{InstructionType, TransferCheckedData}; +use shared::*; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, +}; + +/// Test transfer_checked with SPL Token mint +#[tokio::test] +async fn test_ctoken_transfer_checked_spl_mint() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let decimals = 9u8; + + // Create SPL mint + let mint_keypair = Keypair::new(); + let mint = mint_keypair.pubkey(); + + let mint_rent = rpc + .get_minimum_balance_for_rent_exemption(Mint::LEN) + .await + .unwrap(); + + let create_mint_account_ix = solana_sdk::system_instruction::create_account( + &payer.pubkey(), + &mint, + mint_rent, + Mint::LEN as u64, + &spl_token::ID, + ); + + let initialize_mint_ix = spl_token::instruction::initialize_mint( + &spl_token::ID, + &mint, + &payer.pubkey(), + Some(&payer.pubkey()), + decimals, + ) + .unwrap(); + + rpc.create_and_send_transaction( + &[create_mint_account_ix, initialize_mint_ix], + &payer.pubkey(), + &[&payer, &mint_keypair], + ) + .await + .unwrap(); + + // Create token pool for SPL interface + let create_pool_ix = + CreateSplInterfacePda::new(payer.pubkey(), mint, anchor_spl::token::ID, false) + .instruction(); + + rpc.create_and_send_transaction(&[create_pool_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Create SPL token account and mint tokens + let spl_token_account_keypair = Keypair::new(); + create_token_account(&mut rpc, &mint, &spl_token_account_keypair, &payer) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &spl_token_account_keypair.pubkey(), + &payer.pubkey(), + &payer, + 1000, + false, + ) + .await + .unwrap(); + + // Create cToken ATAs for source and destination + let source_owner = payer.pubkey(); + let dest_owner = Pubkey::new_unique(); + + let (source_ata, _) = derive_token_ata(&source_owner, &mint); + let (dest_ata, _) = derive_token_ata(&dest_owner, &mint); + + let create_source_ata = CreateAssociatedTokenAccount::new(payer.pubkey(), source_owner, mint) + .instruction() + .unwrap(); + let create_dest_ata = CreateAssociatedTokenAccount::new(payer.pubkey(), dest_owner, mint) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction( + &[create_source_ata, create_dest_ata], + &payer.pubkey(), + &[&payer], + ) + .await + .unwrap(); + + // Transfer SPL tokens to source cToken ATA + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let transfer_to_ctoken = TransferFromSpl { + amount: 1000, + spl_interface_pda_bump, + decimals, + source_spl_token_account: spl_token_account_keypair.pubkey(), + destination: source_ata, + authority: payer.pubkey(), + mint, + payer: payer.pubkey(), + spl_interface_pda, + spl_token_program: anchor_spl::token::ID, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[transfer_to_ctoken], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Execute transfer_checked via wrapper program + let transfer_data = TransferCheckedData { + amount: 500, + decimals, + }; + let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; + transfer_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new_readonly(source_owner, true), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify balances + let source_data = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state = Token::deserialize(&mut &source_data.data[..]).unwrap(); + assert_eq!(source_state.amount, 500); + + let dest_data = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state = Token::deserialize(&mut &dest_data.data[..]).unwrap(); + assert_eq!(dest_state.amount, 500); +} + +/// Test transfer_checked with Token-2022 mint +#[tokio::test] +async fn test_ctoken_transfer_checked_t22_mint() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let decimals = 2u8; + + // Create Token-2022 mint with extensions + let (mint_keypair, _extension_config) = + create_mint_22_with_extensions(&mut rpc, &payer, decimals).await; + let mint = mint_keypair.pubkey(); + + // Create T22 token account and mint tokens + let t22_token_account = create_token_22_account(&mut rpc, &payer, &mint, &payer.pubkey()).await; + mint_spl_tokens_22(&mut rpc, &payer, &mint, &t22_token_account, 1000).await; + + // Create cToken ATAs for source and destination with compression_only for T22 restricted extensions + let source_owner = payer.pubkey(); + let dest_owner = Pubkey::new_unique(); + + let (source_ata, _) = derive_token_ata(&source_owner, &mint); + let (dest_ata, _) = derive_token_ata(&dest_owner, &mint); + + use light_token::instruction::CompressibleParams; + let compressible_params = CompressibleParams { + compression_only: true, + ..Default::default() + }; + + let create_source_ata = CreateAssociatedTokenAccount::new(payer.pubkey(), source_owner, mint) + .with_compressible(compressible_params.clone()) + .instruction() + .unwrap(); + let create_dest_ata = CreateAssociatedTokenAccount::new(payer.pubkey(), dest_owner, mint) + .with_compressible(compressible_params) + .instruction() + .unwrap(); + + rpc.create_and_send_transaction( + &[create_source_ata, create_dest_ata], + &payer.pubkey(), + &[&payer], + ) + .await + .unwrap(); + + // Transfer T22 tokens to source cToken ATA (use restricted=true for mints with restricted extensions) + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, true); + let transfer_to_ctoken = TransferFromSpl { + amount: 1000, + spl_interface_pda_bump, + decimals, + source_spl_token_account: t22_token_account, + destination: source_ata, + authority: payer.pubkey(), + mint, + payer: payer.pubkey(), + spl_interface_pda, + spl_token_program: spl_token_2022::ID, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[transfer_to_ctoken], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Execute transfer_checked via wrapper program + let transfer_data = TransferCheckedData { + amount: 500, + decimals, + }; + let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; + transfer_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new_readonly(source_owner, true), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify balances + let source_data = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state = Token::deserialize(&mut &source_data.data[..]).unwrap(); + assert_eq!(source_state.amount, 500); + + let dest_data = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state = Token::deserialize(&mut &dest_data.data[..]).unwrap(); + assert_eq!(dest_state.amount, 500); +} + +/// Test transfer_checked with decompressed Mint +#[tokio::test] +async fn test_ctoken_transfer_checked_mint() { + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + ); + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let decimals = 9u8; + let source_owner = payer.pubkey(); + let dest_owner = Pubkey::new_unique(); + + // Create compressed mint and decompress it, then create ATAs with tokens + let (mint, _compression_address, ata_pubkeys) = setup_create_mint_with_freeze_authority( + &mut rpc, + &payer, + payer.pubkey(), + None, // no freeze authority needed for transfer + decimals, + vec![(1000, source_owner), (0, dest_owner)], + ) + .await; + + let source_ata = ata_pubkeys[0]; + let dest_ata = ata_pubkeys[1]; + + // Execute transfer_checked via wrapper program + let transfer_data = TransferCheckedData { + amount: 500, + decimals, + }; + let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; + transfer_data.serialize(&mut instruction_data).unwrap(); + + let light_token_program = light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: vec![ + AccountMeta::new(source_ata, false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(dest_ata, false), + AccountMeta::new_readonly(source_owner, true), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), + ], + data: instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify balances + let source_data = rpc.get_account(source_ata).await.unwrap().unwrap(); + let source_state = Token::deserialize(&mut &source_data.data[..]).unwrap(); + assert_eq!(source_state.amount, 500); + + let dest_data = rpc.get_account(dest_ata).await.unwrap().unwrap(); + let dest_state = Token::deserialize(&mut &dest_data.data[..]).unwrap(); + assert_eq!(dest_state.amount, 500); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_interface.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_interface.rs new file mode 100644 index 0000000000..197c361b08 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_interface.rs @@ -0,0 +1,1311 @@ +// Tests for TransferInterfaceCpi - unified transfer interface that auto-detects account types + +mod shared; + +use borsh::BorshSerialize; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_test_utils::spl::{ + create_mint_helper, create_token_2022_account, mint_spl_tokens, CREATE_MINT_HELPER_DECIMALS, +}; +use light_token::{ + instruction::{derive_token_ata, CompressibleParams, CreateAssociatedTokenAccount}, + spl_interface::find_spl_interface_pda_with_index, +}; +use light_token_types::CPI_AUTHORITY_PDA; +use sdk_light_token_pinocchio_test::{TransferInterfaceData, TRANSFER_INTERFACE_AUTHORITY_SEED}; +use shared::PROGRAM_ID; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, +}; + +// ============================================================================= +// INVOKE TESTS (regular signer authority) +// ============================================================================= + +/// Test TransferInterfaceCpi: SPL -> Light Token (invoke) +#[tokio::test] +async fn test_transfer_interface_spl_to_ctoken_invoke() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let sender = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &sender.pubkey(), 1_000_000_000) + .await + .unwrap(); + + // Create SPL mint and token account + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + let spl_token_account_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &spl_token_account_keypair, &sender, false) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &spl_token_account_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + // Create Light Token ATA for recipient + let recipient = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), recipient.pubkey(), mint) + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + let ctoken_account = derive_token_ata(&recipient.pubkey(), &mint).0; + + // Get token pool PDA + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + // Build wrapper instruction + let data = TransferInterfaceData { + amount: transfer_amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 19 = TransferInterfaceInvoke + let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); + + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(spl_token_account_keypair.pubkey(), false), // source (SPL) + AccountMeta::new(ctoken_account, false), // destination (Light Token) + AccountMeta::new_readonly(sender.pubkey(), true), // authority (signer) + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(cpi_authority_pda, false), // compressed_token_program_authority + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(mint, false), // mint (for SPL bridge) + AccountMeta::new(spl_interface_pda, false), // spl_interface_pda + AccountMeta::new_readonly(anchor_spl::token::ID, false), // spl_token_program + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &sender]) + .await + .unwrap(); + + // Verify balances + use spl_token_2022::pod::PodAccount; + let spl_account_data = rpc + .get_account(spl_token_account_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let spl_account = + spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data).unwrap(); + assert_eq!(u64::from(spl_account.amount), amount - transfer_amount); + + let ctoken_account_data = rpc.get_account(ctoken_account).await.unwrap().unwrap(); + let ctoken_state = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]).unwrap(); + assert_eq!(u64::from(ctoken_state.amount), transfer_amount); + + println!("TransferInterface SPL->Light Token invoke test passed"); +} + +/// Test TransferInterface: Light Token -> SPL (invoke) +#[tokio::test] +async fn test_transfer_interface_ctoken_to_spl_invoke() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let owner = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &owner.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create destination SPL token account + let spl_token_account_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &spl_token_account_keypair, &owner, false) + .await + .unwrap(); + + // Create and fund Light Token ATA + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), owner.pubkey(), mint) + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + let ctoken_account = derive_token_ata(&owner.pubkey(), &mint).0; + + // Fund Light Token via temporary SPL account + let temp_spl_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &temp_spl_keypair, &owner, false) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &temp_spl_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + // Transfer SPL to Light Token to fund it + { + let data = TransferInterfaceData { + amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(temp_spl_keypair.pubkey(), false), + AccountMeta::new(ctoken_account, false), + AccountMeta::new_readonly(owner.pubkey(), true), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + ]; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner]) + .await + .unwrap(); + } + + // Now test Light Token -> SPL transfer + let data = TransferInterfaceData { + amount: transfer_amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); + + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(ctoken_account, false), // source (Light Token) + AccountMeta::new(spl_token_account_keypair.pubkey(), false), // destination (SPL) + AccountMeta::new_readonly(owner.pubkey(), true), // authority + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner]) + .await + .unwrap(); + + // Verify balances + use spl_token_2022::pod::PodAccount; + let spl_account_data = rpc + .get_account(spl_token_account_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let spl_account = + spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data).unwrap(); + assert_eq!(u64::from(spl_account.amount), transfer_amount); + + let ctoken_account_data = rpc.get_account(ctoken_account).await.unwrap().unwrap(); + let ctoken_state = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]).unwrap(); + assert_eq!(u64::from(ctoken_state.amount), amount - transfer_amount); + + println!("TransferInterface Light Token->SPL invoke test passed"); +} + +/// Test TransferInterface: Light Token -> Light Token (invoke) +#[tokio::test] +async fn test_transfer_interface_ctoken_to_ctoken_invoke() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let sender = Keypair::new(); + let recipient = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &sender.pubkey(), 1_000_000_000) + .await + .unwrap(); + light_test_utils::airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create sender Light Token ATA + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), sender.pubkey(), mint) + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + let sender_ctoken = derive_token_ata(&sender.pubkey(), &mint).0; + + // Create recipient Light Token ATA + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), recipient.pubkey(), mint) + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + let recipient_ctoken = derive_token_ata(&recipient.pubkey(), &mint).0; + + // Fund sender Light Token via SPL + let temp_spl_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &temp_spl_keypair, &sender, false) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &temp_spl_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + // Fund sender Light Token + { + let data = TransferInterfaceData { + amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(temp_spl_keypair.pubkey(), false), + AccountMeta::new(sender_ctoken, false), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + ]; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &sender]) + .await + .unwrap(); + } + + // Now test Light Token -> Light Token transfer (no SPL bridge needed) + let data = TransferInterfaceData { + amount: transfer_amount, + spl_interface_pda_bump: None, // Not needed for Light Token->Light Token + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); + + // For Light Token->Light Token, we need 7 accounts (no SPL bridge, but system_program is required) + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(sender_ctoken, false), // source (Light Token) + AccountMeta::new(recipient_ctoken, false), // destination (Light Token) + AccountMeta::new(sender.pubkey(), true), // authority (writable, signer) + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &sender]) + .await + .unwrap(); + + // Verify balances + use spl_token_2022::pod::PodAccount; + let sender_ctoken_data = rpc.get_account(sender_ctoken).await.unwrap().unwrap(); + let sender_state = + spl_pod::bytemuck::pod_from_bytes::(&sender_ctoken_data.data[..165]).unwrap(); + assert_eq!(u64::from(sender_state.amount), amount - transfer_amount); + + let recipient_ctoken_data = rpc.get_account(recipient_ctoken).await.unwrap().unwrap(); + let recipient_state = + spl_pod::bytemuck::pod_from_bytes::(&recipient_ctoken_data.data[..165]) + .unwrap(); + assert_eq!(u64::from(recipient_state.amount), transfer_amount); + + println!("TransferInterface Light Token->Light Token invoke test passed"); +} + +// ============================================================================= +// INVOKE_SIGNED TESTS (PDA authority) +// ============================================================================= + +/// Test TransferInterface: SPL -> Light Token with PDA authority (invoke_signed) +#[tokio::test] +async fn test_transfer_interface_spl_to_ctoken_invoke_signed() { + use anchor_spl::associated_token::{ + get_associated_token_address, spl_associated_token_account, + }; + + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + // Derive PDA authority + let (authority_pda, _) = + Pubkey::find_program_address(&[TRANSFER_INTERFACE_AUTHORITY_SEED], &PROGRAM_ID); + + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create SPL ATA owned by PDA + let spl_ata = get_associated_token_address(&authority_pda, &mint); + let create_ata_ix = spl_associated_token_account::instruction::create_associated_token_account( + &payer.pubkey(), + &authority_pda, + &mint, + &anchor_spl::token::ID, + ); + rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Mint tokens to PDA's ATA + mint_spl_tokens( + &mut rpc, + &mint, + &spl_ata, + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + // Create destination Light Token ATA + let recipient = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), recipient.pubkey(), mint) + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + let ctoken_account = derive_token_ata(&recipient.pubkey(), &mint).0; + + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + let data = TransferInterfaceData { + amount: transfer_amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 20 = TransferInterfaceInvokeSigned + let wrapper_instruction_data = [vec![20u8], data.try_to_vec().unwrap()].concat(); + + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(spl_ata, false), // source (SPL owned by PDA) + AccountMeta::new(ctoken_account, false), // destination (Light Token) + AccountMeta::new_readonly(authority_pda, false), // authority (PDA, not signer) + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(cpi_authority_pda, false), // compressed_token_program_authority + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(mint, false), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify balances + use spl_token_2022::pod::PodAccount; + let spl_account_data = rpc.get_account(spl_ata).await.unwrap().unwrap(); + let spl_account = + spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data).unwrap(); + assert_eq!(u64::from(spl_account.amount), amount - transfer_amount); + + let ctoken_account_data = rpc.get_account(ctoken_account).await.unwrap().unwrap(); + let ctoken_state = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]).unwrap(); + assert_eq!(u64::from(ctoken_state.amount), transfer_amount); + + println!("TransferInterface SPL->Light Token invoke_signed test passed"); +} + +/// Test TransferInterface: Light Token -> SPL with PDA authority (invoke_signed) +#[tokio::test] +async fn test_transfer_interface_ctoken_to_spl_invoke_signed() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + // Derive PDA authority + let (authority_pda, _) = + Pubkey::find_program_address(&[TRANSFER_INTERFACE_AUTHORITY_SEED], &PROGRAM_ID); + + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create destination SPL token account + let destination_owner = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &destination_owner.pubkey(), 1_000_000_000) + .await + .unwrap(); + let spl_token_account_keypair = Keypair::new(); + create_token_2022_account( + &mut rpc, + &mint, + &spl_token_account_keypair, + &destination_owner, + false, + ) + .await + .unwrap(); + + // Create Light Token ATA owned by PDA + let (ctoken_account, bump) = derive_token_ata(&authority_pda, &mint); + let instruction = CreateAssociatedTokenAccount { + idempotent: false, + bump, + payer: payer.pubkey(), + owner: authority_pda, + mint, + associated_token_account: ctoken_account, + compressible: CompressibleParams::default_ata(), + } + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Fund PDA's Light Token via temporary SPL account + let temp_spl_keypair = Keypair::new(); + let temp_owner = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &temp_owner.pubkey(), 1_000_000_000) + .await + .unwrap(); + create_token_2022_account(&mut rpc, &mint, &temp_spl_keypair, &temp_owner, false) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &temp_spl_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + // Fund PDA's Light Token + { + let data = TransferInterfaceData { + amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(temp_spl_keypair.pubkey(), false), + AccountMeta::new(ctoken_account, false), + AccountMeta::new_readonly(temp_owner.pubkey(), true), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + ]; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &temp_owner]) + .await + .unwrap(); + } + + // Now test Light Token -> SPL with PDA authority + let data = TransferInterfaceData { + amount: transfer_amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 20 = TransferInterfaceInvokeSigned + let wrapper_instruction_data = [vec![20u8], data.try_to_vec().unwrap()].concat(); + + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(ctoken_account, false), // source (Light Token owned by PDA) + AccountMeta::new(spl_token_account_keypair.pubkey(), false), // destination (SPL) + AccountMeta::new_readonly(authority_pda, false), // authority (PDA) + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify balances + use spl_token_2022::pod::PodAccount; + let spl_account_data = rpc + .get_account(spl_token_account_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let spl_account = + spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data).unwrap(); + assert_eq!(u64::from(spl_account.amount), transfer_amount); + + let ctoken_account_data = rpc.get_account(ctoken_account).await.unwrap().unwrap(); + let ctoken_state = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]).unwrap(); + assert_eq!(u64::from(ctoken_state.amount), amount - transfer_amount); + + println!("TransferInterface Light Token->SPL invoke_signed test passed"); +} + +/// Test TransferInterface: Light Token -> Light Token with PDA authority (invoke_signed) +#[tokio::test] +async fn test_transfer_interface_ctoken_to_ctoken_invoke_signed() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + // Derive PDA authority + let (authority_pda, _) = + Pubkey::find_program_address(&[TRANSFER_INTERFACE_AUTHORITY_SEED], &PROGRAM_ID); + + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create source Light Token ATA owned by PDA + let (source_ctoken, bump) = derive_token_ata(&authority_pda, &mint); + let instruction = CreateAssociatedTokenAccount { + idempotent: false, + bump, + payer: payer.pubkey(), + owner: authority_pda, + mint, + associated_token_account: source_ctoken, + compressible: CompressibleParams::default_ata(), + } + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Create destination Light Token ATA + let recipient = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), recipient.pubkey(), mint) + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + let dest_ctoken = derive_token_ata(&recipient.pubkey(), &mint).0; + + // Fund source Light Token via temporary SPL account + let temp_spl_keypair = Keypair::new(); + let temp_owner = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &temp_owner.pubkey(), 1_000_000_000) + .await + .unwrap(); + create_token_2022_account(&mut rpc, &mint, &temp_spl_keypair, &temp_owner, false) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &temp_spl_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + // Fund source Light Token + { + let data = TransferInterfaceData { + amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(temp_spl_keypair.pubkey(), false), + AccountMeta::new(source_ctoken, false), + AccountMeta::new_readonly(temp_owner.pubkey(), true), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + ]; + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &temp_owner]) + .await + .unwrap(); + } + + // Now test Light Token -> Light Token with PDA authority + let data = TransferInterfaceData { + amount: transfer_amount, + spl_interface_pda_bump: None, // Not needed for Light Token->Light Token + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 20 = TransferInterfaceInvokeSigned + let wrapper_instruction_data = [vec![20u8], data.try_to_vec().unwrap()].concat(); + + // For Light Token->Light Token, we only need 6 accounts (no SPL bridge) + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(source_ctoken, false), // source (Light Token owned by PDA) + AccountMeta::new(dest_ctoken, false), // destination (Light Token) + AccountMeta::new(authority_pda, false), // authority (PDA, writable, program signs) + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify balances + use spl_token_2022::pod::PodAccount; + let source_ctoken_data = rpc.get_account(source_ctoken).await.unwrap().unwrap(); + let source_state = + spl_pod::bytemuck::pod_from_bytes::(&source_ctoken_data.data[..165]).unwrap(); + assert_eq!(u64::from(source_state.amount), amount - transfer_amount); + + let dest_ctoken_data = rpc.get_account(dest_ctoken).await.unwrap().unwrap(); + let dest_state = + spl_pod::bytemuck::pod_from_bytes::(&dest_ctoken_data.data[..165]).unwrap(); + assert_eq!(u64::from(dest_state.amount), transfer_amount); + + println!("TransferInterface Light Token->Light Token invoke_signed test passed"); +} + +// ============================================================================= +// SPL-TO-SPL TESTS +// ============================================================================= + +/// Test TransferInterface: SPL -> SPL (invoke) +#[tokio::test] +async fn test_transfer_interface_spl_to_spl_invoke() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let sender = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &sender.pubkey(), 1_000_000_000) + .await + .unwrap(); + + // Create SPL mint and token accounts + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create source SPL token account + let source_spl_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &source_spl_keypair, &sender, false) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &source_spl_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + // Create destination SPL token account + let recipient = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + let dest_spl_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &dest_spl_keypair, &recipient, false) + .await + .unwrap(); + + // Get SPL interface PDA (not actually used for SPL->SPL, but needed by wrapper) + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + // Build wrapper instruction for SPL->SPL transfer + let data = TransferInterfaceData { + amount: transfer_amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 19 = TransferInterfaceInvoke + let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); + + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(source_spl_keypair.pubkey(), false), // source (SPL) + AccountMeta::new(dest_spl_keypair.pubkey(), false), // destination (SPL) + AccountMeta::new_readonly(sender.pubkey(), true), // authority (signer) + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(cpi_authority_pda, false), // compressed_token_program_authority + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(mint, false), // mint (for SPL transfer_checked) + AccountMeta::new(spl_interface_pda, false), // spl_interface_pda (passed but not used) + AccountMeta::new_readonly(anchor_spl::token::ID, false), // spl_token_program + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &sender]) + .await + .unwrap(); + + // Verify balances + use spl_token_2022::pod::PodAccount; + let source_account_data = rpc + .get_account(source_spl_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let source_account = + spl_pod::bytemuck::pod_from_bytes::(&source_account_data.data).unwrap(); + assert_eq!(u64::from(source_account.amount), amount - transfer_amount); + + let dest_account_data = rpc + .get_account(dest_spl_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let dest_account = + spl_pod::bytemuck::pod_from_bytes::(&dest_account_data.data).unwrap(); + assert_eq!(u64::from(dest_account.amount), transfer_amount); + + println!("TransferInterface SPL->SPL invoke test passed"); +} + +/// Test TransferInterface: SPL -> SPL with PDA authority (invoke_signed) +#[tokio::test] +async fn test_transfer_interface_spl_to_spl_invoke_signed() { + use anchor_spl::associated_token::{ + get_associated_token_address, spl_associated_token_account, + }; + + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + // Derive PDA authority + let (authority_pda, _) = + Pubkey::find_program_address(&[TRANSFER_INTERFACE_AUTHORITY_SEED], &PROGRAM_ID); + + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create SPL ATA owned by PDA + let source_spl_ata = get_associated_token_address(&authority_pda, &mint); + let create_ata_ix = spl_associated_token_account::instruction::create_associated_token_account( + &payer.pubkey(), + &authority_pda, + &mint, + &anchor_spl::token::ID, + ); + rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Mint tokens to PDA's ATA + mint_spl_tokens( + &mut rpc, + &mint, + &source_spl_ata, + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + // Create destination SPL token account + let recipient = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + let dest_spl_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &dest_spl_keypair, &recipient, false) + .await + .unwrap(); + + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + let data = TransferInterfaceData { + amount: transfer_amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 20 = TransferInterfaceInvokeSigned + let wrapper_instruction_data = [vec![20u8], data.try_to_vec().unwrap()].concat(); + + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(source_spl_ata, false), // source (SPL owned by PDA) + AccountMeta::new(dest_spl_keypair.pubkey(), false), // destination (SPL) + AccountMeta::new_readonly(authority_pda, false), // authority (PDA, not signer) + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(cpi_authority_pda, false), // compressed_token_program_authority + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(mint, false), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify balances + use spl_token_2022::pod::PodAccount; + let source_account_data = rpc.get_account(source_spl_ata).await.unwrap().unwrap(); + let source_account = + spl_pod::bytemuck::pod_from_bytes::(&source_account_data.data).unwrap(); + assert_eq!(u64::from(source_account.amount), amount - transfer_amount); + + let dest_account_data = rpc + .get_account(dest_spl_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let dest_account = + spl_pod::bytemuck::pod_from_bytes::(&dest_account_data.data).unwrap(); + assert_eq!(u64::from(dest_account.amount), transfer_amount); + + println!("TransferInterface SPL->SPL invoke_signed test passed"); +} + +// ============================================================================= +// TOKEN-2022 TO TOKEN-2022 TESTS +// ============================================================================= + +/// Test TransferInterface: T22 -> T22 (invoke) +#[tokio::test] +async fn test_transfer_interface_t22_to_t22_invoke() { + use light_test_utils::spl::create_mint_22_helper; + + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let sender = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &sender.pubkey(), 1_000_000_000) + .await + .unwrap(); + + // Create T22 mint and token accounts + let mint = create_mint_22_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create source T22 token account + let source_t22_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &source_t22_keypair, &sender, true) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &source_t22_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + true, + ) + .await + .unwrap(); + + // Create destination T22 token account + let recipient = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + let dest_t22_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &dest_t22_keypair, &recipient, true) + .await + .unwrap(); + + // Get SPL interface PDA for T22 + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, true); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + // Build wrapper instruction for T22->T22 transfer + let data = TransferInterfaceData { + amount: transfer_amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 19 = TransferInterfaceInvoke + let wrapper_instruction_data = [vec![19u8], data.try_to_vec().unwrap()].concat(); + + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(source_t22_keypair.pubkey(), false), // source (T22) + AccountMeta::new(dest_t22_keypair.pubkey(), false), // destination (T22) + AccountMeta::new_readonly(sender.pubkey(), true), // authority (signer) + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(cpi_authority_pda, false), // compressed_token_program_authority + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(mint, false), // mint (for T22 transfer_checked) + AccountMeta::new(spl_interface_pda, false), // spl_interface_pda (passed but not used) + AccountMeta::new_readonly(anchor_spl::token_2022::ID, false), // T22 token program + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &sender]) + .await + .unwrap(); + + // Verify balances using T22 state unpacking (handles extensions) + use spl_token_2022::{extension::StateWithExtensions, state::Account as T22Account}; + + let source_account_data = rpc + .get_account(source_t22_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let source_state = + StateWithExtensions::::unpack(&source_account_data.data).unwrap(); + assert_eq!(source_state.base.amount, amount - transfer_amount); + + let dest_account_data = rpc + .get_account(dest_t22_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let dest_state = StateWithExtensions::::unpack(&dest_account_data.data).unwrap(); + assert_eq!(dest_state.base.amount, transfer_amount); + + println!("TransferInterface T22->T22 invoke test passed"); +} + +/// Test TransferInterface: T22 -> T22 with PDA authority (invoke_signed) +#[tokio::test] +async fn test_transfer_interface_t22_to_t22_invoke_signed() { + use anchor_spl::associated_token::{ + get_associated_token_address_with_program_id, spl_associated_token_account, + }; + use light_test_utils::spl::create_mint_22_helper; + + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + // Derive PDA authority + let (authority_pda, _) = + Pubkey::find_program_address(&[TRANSFER_INTERFACE_AUTHORITY_SEED], &PROGRAM_ID); + + let mint = create_mint_22_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create T22 ATA owned by PDA + let source_t22_ata = get_associated_token_address_with_program_id( + &authority_pda, + &mint, + &anchor_spl::token_2022::ID, + ); + let create_ata_ix = spl_associated_token_account::instruction::create_associated_token_account( + &payer.pubkey(), + &authority_pda, + &mint, + &anchor_spl::token_2022::ID, + ); + rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Mint tokens to PDA's ATA + mint_spl_tokens( + &mut rpc, + &mint, + &source_t22_ata, + &payer.pubkey(), + &payer, + amount, + true, + ) + .await + .unwrap(); + + // Create destination T22 token account + let recipient = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + let dest_t22_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &dest_t22_keypair, &recipient, true) + .await + .unwrap(); + + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, true); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + let data = TransferInterfaceData { + amount: transfer_amount, + spl_interface_pda_bump: Some(spl_interface_pda_bump), + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 20 = TransferInterfaceInvokeSigned + let wrapper_instruction_data = [vec![20u8], data.try_to_vec().unwrap()].concat(); + + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(source_t22_ata, false), // source (T22 owned by PDA) + AccountMeta::new(dest_t22_keypair.pubkey(), false), // destination (T22) + AccountMeta::new_readonly(authority_pda, false), // authority (PDA, not signer) + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(cpi_authority_pda, false), // compressed_token_program_authority + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(mint, false), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token_2022::ID, false), // T22 token program + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify balances using T22 state unpacking (handles extensions) + use spl_token_2022::{extension::StateWithExtensions, state::Account as T22Account}; + + let source_account_data = rpc.get_account(source_t22_ata).await.unwrap().unwrap(); + let source_state = + StateWithExtensions::::unpack(&source_account_data.data).unwrap(); + assert_eq!(source_state.base.amount, amount - transfer_amount); + + let dest_account_data = rpc + .get_account(dest_t22_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let dest_state = StateWithExtensions::::unpack(&dest_account_data.data).unwrap(); + assert_eq!(dest_state.base.amount, transfer_amount); + + println!("TransferInterface T22->T22 invoke_signed test passed"); +} diff --git a/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_spl_ctoken.rs b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_spl_ctoken.rs new file mode 100644 index 0000000000..4b03365d29 --- /dev/null +++ b/sdk-tests/sdk-light-token-pinocchio/tests/test_transfer_spl_ctoken.rs @@ -0,0 +1,629 @@ +// Tests for TransferFromSplCpi and TransferTokenToSplCpi + +mod shared; + +use borsh::BorshSerialize; +use light_client::rpc::Rpc; +use light_program_test::{LightProgramTest, ProgramTestConfig}; +use light_test_utils::spl::{ + create_mint_helper, create_token_2022_account, mint_spl_tokens, CREATE_MINT_HELPER_DECIMALS, +}; +use light_token::{ + instruction::{derive_token_ata, CompressibleParams, CreateAssociatedTokenAccount}, + spl_interface::find_spl_interface_pda_with_index, +}; +use light_token_types::CPI_AUTHORITY_PDA; +use sdk_light_token_pinocchio_test::{ + TransferFromSplData, TransferTokenToSplData, TRANSFER_AUTHORITY_SEED, +}; +use shared::PROGRAM_ID; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::Keypair, + signer::Signer, +}; + +/// Test transferring SPL tokens to Light Token using TransferFromSplCpi::invoke() +#[tokio::test] +async fn test_spl_to_ctoken_invoke() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let sender = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &sender.pubkey(), 1_000_000_000) + .await + .unwrap(); + + // Create SPL mint + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create SPL token account and mint tokens + let spl_token_account_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &spl_token_account_keypair, &sender, false) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &spl_token_account_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + // Create compressed token ATA for recipient + let recipient = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), recipient.pubkey(), mint) + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + let ctoken_account = derive_token_ata(&recipient.pubkey(), &mint).0; + + // Get initial balances + use spl_token_2022::pod::PodAccount; + let spl_account_data = rpc + .get_account(spl_token_account_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let spl_account = + spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data).unwrap(); + let initial_spl_balance: u64 = spl_account.amount.into(); + assert_eq!(initial_spl_balance, amount); + + // Get token pool PDA + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + // Build wrapper instruction for SPL to Light Token transfer + let data = TransferFromSplData { + amount: transfer_amount, + spl_interface_pda_bump, + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 15 = SplToCtokenInvoke + let wrapper_instruction_data = [vec![15u8], data.try_to_vec().unwrap()].concat(); + + // Account order from handler: + // - accounts[0]: compressed_token_program (for CPI) + // - accounts[1]: source_spl_token_account + // - accounts[2]: destination (writable) + // - accounts[3]: authority (signer) + // - accounts[4]: mint + // - accounts[5]: payer (signer) + // - accounts[6]: spl_interface_pda + // - accounts[7]: spl_token_program + // - accounts[8]: compressed_token_program_authority + // - accounts[9]: system_program + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(spl_token_account_keypair.pubkey(), false), + AccountMeta::new(ctoken_account, false), // destination (writable) + AccountMeta::new_readonly(sender.pubkey(), true), // authority (signer) + AccountMeta::new_readonly(mint, false), + AccountMeta::new(payer.pubkey(), true), // payer (signer) + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &sender]) + .await + .unwrap(); + + // Verify SPL token balance decreased + let spl_account_data = rpc + .get_account(spl_token_account_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let spl_account = + spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data).unwrap(); + let final_spl_balance: u64 = spl_account.amount.into(); + assert_eq!(final_spl_balance, amount - transfer_amount); + + // Verify Light Token balance increased + let ctoken_account_data = rpc.get_account(ctoken_account).await.unwrap().unwrap(); + let ctoken_account_state = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]).unwrap(); + assert_eq!( + u64::from(ctoken_account_state.amount), + transfer_amount, + "Light Token account should have received tokens" + ); + + println!("SPL to Light Token invoke test passed"); +} + +/// Test transferring Light Token to SPL tokens using TransferTokenToSplCpi::invoke() +#[tokio::test] +async fn test_ctoken_to_spl_invoke() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + let owner = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &owner.pubkey(), 1_000_000_000) + .await + .unwrap(); + + // Create SPL mint + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create SPL token account for receiving back tokens + let spl_token_account_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &spl_token_account_keypair, &owner, false) + .await + .unwrap(); + + // Create ctoken ATA and fund it via SPL transfer first + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), owner.pubkey(), mint) + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + let ctoken_account = derive_token_ata(&owner.pubkey(), &mint).0; + + // Create a temporary SPL account to mint tokens then transfer to ctoken + let temp_spl_account_keypair = Keypair::new(); + create_token_2022_account(&mut rpc, &mint, &temp_spl_account_keypair, &owner, false) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &temp_spl_account_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + // Transfer from temp SPL to ctoken to fund it + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + { + let data = TransferFromSplData { + amount, + spl_interface_pda_bump, + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + let wrapper_instruction_data = [vec![15u8], data.try_to_vec().unwrap()].concat(); + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(temp_spl_account_keypair.pubkey(), false), + AccountMeta::new(ctoken_account, false), // destination (writable) + AccountMeta::new_readonly(owner.pubkey(), true), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner]) + .await + .unwrap(); + } + + // Verify ctoken has tokens + use spl_token_2022::pod::PodAccount; + let ctoken_account_data = rpc.get_account(ctoken_account).await.unwrap().unwrap(); + let ctoken_state = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]).unwrap(); + assert_eq!(u64::from(ctoken_state.amount), amount); + + // Now test Light Token to SPL transfer + let data = TransferTokenToSplData { + amount: transfer_amount, + spl_interface_pda_bump, + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 17 = CtokenToSplInvoke + let wrapper_instruction_data = [vec![17u8], data.try_to_vec().unwrap()].concat(); + + // Account order from handler: + // - accounts[0]: compressed_token_program (for CPI) + // - accounts[1]: source + // - accounts[2]: destination_spl_token_account + // - accounts[3]: authority (signer) + // - accounts[4]: mint + // - accounts[5]: payer (signer) + // - accounts[6]: spl_interface_pda + // - accounts[7]: spl_token_program + // - accounts[8]: compressed_token_program_authority + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(ctoken_account, false), + AccountMeta::new(spl_token_account_keypair.pubkey(), false), + AccountMeta::new_readonly(owner.pubkey(), true), // authority (signer) + AccountMeta::new_readonly(mint, false), + AccountMeta::new(payer.pubkey(), true), // payer (signer) + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + AccountMeta::new_readonly(cpi_authority_pda, false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &owner]) + .await + .unwrap(); + + // Verify SPL token balance increased + let spl_account_data = rpc + .get_account(spl_token_account_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let spl_account = + spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data).unwrap(); + let final_spl_balance: u64 = spl_account.amount.into(); + assert_eq!(final_spl_balance, transfer_amount); + + // Verify Light Token balance decreased + let ctoken_account_data = rpc.get_account(ctoken_account).await.unwrap().unwrap(); + let ctoken_state = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]).unwrap(); + assert_eq!( + u64::from(ctoken_state.amount), + amount - transfer_amount, + "Light Token account balance should have decreased" + ); + + println!("Light Token to SPL invoke test passed"); +} + +/// Test transferring SPL tokens to Light Token with PDA authority using invoke_signed +#[tokio::test] +async fn test_spl_to_ctoken_invoke_signed() { + use anchor_spl::associated_token::{ + get_associated_token_address, spl_associated_token_account, + }; + + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + // Derive the PDA that will be the authority (owner) for the SPL token account + let (authority_pda, _) = Pubkey::find_program_address(&[TRANSFER_AUTHORITY_SEED], &PROGRAM_ID); + + // Create SPL mint + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create SPL ATA owned by the PDA using standard SPL ATA program + let spl_ata = get_associated_token_address(&authority_pda, &mint); + let create_ata_ix = spl_associated_token_account::instruction::create_associated_token_account( + &payer.pubkey(), + &authority_pda, + &mint, + &anchor_spl::token::ID, + ); + rpc.create_and_send_transaction(&[create_ata_ix], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Mint tokens to the PDA's ATA (we're the mint authority so we can mint directly) + mint_spl_tokens( + &mut rpc, + &mint, + &spl_ata, + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + // Create compressed token ATA for recipient + let recipient = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &recipient.pubkey(), 1_000_000_000) + .await + .unwrap(); + + let instruction = CreateAssociatedTokenAccount::new(payer.pubkey(), recipient.pubkey(), mint) + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + let ctoken_account = derive_token_ata(&recipient.pubkey(), &mint).0; + + // Get SPL interface PDA + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + // Build wrapper instruction for SPL to Light Token transfer with PDA authority + let data = TransferFromSplData { + amount: transfer_amount, + spl_interface_pda_bump, + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 16 = SplToCtokenInvokeSigned + let wrapper_instruction_data = [vec![16u8], data.try_to_vec().unwrap()].concat(); + + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(spl_ata, false), + AccountMeta::new(ctoken_account, false), // destination (writable) + AccountMeta::new_readonly(authority_pda, false), // authority is PDA, not signer + AccountMeta::new_readonly(mint, false), + AccountMeta::new(payer.pubkey(), true), // payer (signer) + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify SPL token balance decreased + use spl_token_2022::pod::PodAccount; + let spl_account_data = rpc.get_account(spl_ata).await.unwrap().unwrap(); + let spl_account = + spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data).unwrap(); + let final_spl_balance: u64 = spl_account.amount.into(); + assert_eq!(final_spl_balance, amount - transfer_amount); + + // Verify Light Token balance increased + let ctoken_account_data = rpc.get_account(ctoken_account).await.unwrap().unwrap(); + let ctoken_account_state = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]).unwrap(); + assert_eq!( + u64::from(ctoken_account_state.amount), + transfer_amount, + "Light Token account should have received tokens" + ); + + println!("SPL to Light Token invoke_signed test passed"); +} + +/// Test transferring Light Token to SPL with PDA authority using invoke_signed +#[tokio::test] +async fn test_ctoken_to_spl_invoke_signed() { + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_light_token_pinocchio_test", PROGRAM_ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + // Derive the PDA that will be the authority + let (authority_pda, _) = Pubkey::find_program_address(&[TRANSFER_AUTHORITY_SEED], &PROGRAM_ID); + + // Create SPL mint + let mint = create_mint_helper(&mut rpc, &payer).await; + let amount = 10000u64; + let transfer_amount = 5000u64; + + // Create SPL token account for receiving tokens + let spl_token_account_keypair = Keypair::new(); + let destination_owner = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &destination_owner.pubkey(), 1_000_000_000) + .await + .unwrap(); + create_token_2022_account( + &mut rpc, + &mint, + &spl_token_account_keypair, + &destination_owner, + false, + ) + .await + .unwrap(); + + // Create ctoken ATA owned by the PDA + let (ctoken_account, bump) = derive_token_ata(&authority_pda, &mint); + let instruction = CreateAssociatedTokenAccount { + idempotent: false, + bump, + payer: payer.pubkey(), + owner: authority_pda, + mint, + associated_token_account: ctoken_account, + compressible: CompressibleParams::default_ata(), + } + .instruction() + .unwrap(); + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Fund the ctoken account via SPL transfer from a temporary account + let temp_spl_account_keypair = Keypair::new(); + let temp_owner = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &temp_owner.pubkey(), 1_000_000_000) + .await + .unwrap(); + create_token_2022_account( + &mut rpc, + &mint, + &temp_spl_account_keypair, + &temp_owner, + false, + ) + .await + .unwrap(); + mint_spl_tokens( + &mut rpc, + &mint, + &temp_spl_account_keypair.pubkey(), + &payer.pubkey(), + &payer, + amount, + false, + ) + .await + .unwrap(); + + // Transfer from temp SPL to ctoken to fund it + let (spl_interface_pda, spl_interface_pda_bump) = + find_spl_interface_pda_with_index(&mint, 0, false); + let compressed_token_program_id = + Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + let cpi_authority_pda = Pubkey::new_from_array(CPI_AUTHORITY_PDA); + + { + let data = TransferFromSplData { + amount, + spl_interface_pda_bump, + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + let wrapper_instruction_data = [vec![15u8], data.try_to_vec().unwrap()].concat(); + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(temp_spl_account_keypair.pubkey(), false), + AccountMeta::new(ctoken_account, false), // destination (writable) + AccountMeta::new_readonly(temp_owner.pubkey(), true), + AccountMeta::new_readonly(mint, false), + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + AccountMeta::new_readonly(cpi_authority_pda, false), + AccountMeta::new_readonly(Pubkey::default(), false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &temp_owner]) + .await + .unwrap(); + } + + // Verify ctoken has tokens + use spl_token_2022::pod::PodAccount; + let ctoken_account_data = rpc.get_account(ctoken_account).await.unwrap().unwrap(); + let ctoken_state = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]).unwrap(); + assert_eq!(u64::from(ctoken_state.amount), amount); + + // Now test Light Token to SPL transfer with PDA authority + let data = TransferTokenToSplData { + amount: transfer_amount, + spl_interface_pda_bump, + decimals: CREATE_MINT_HELPER_DECIMALS, + }; + // Discriminator 18 = CtokenToSplInvokeSigned + let wrapper_instruction_data = [vec![18u8], data.try_to_vec().unwrap()].concat(); + + let wrapper_accounts = vec![ + AccountMeta::new_readonly(compressed_token_program_id, false), + AccountMeta::new(ctoken_account, false), + AccountMeta::new(spl_token_account_keypair.pubkey(), false), + AccountMeta::new_readonly(authority_pda, false), // authority is PDA, not signer + AccountMeta::new_readonly(mint, false), + AccountMeta::new(payer.pubkey(), true), // payer (signer) + AccountMeta::new(spl_interface_pda, false), + AccountMeta::new_readonly(anchor_spl::token::ID, false), + AccountMeta::new_readonly(cpi_authority_pda, false), + ]; + + let instruction = Instruction { + program_id: PROGRAM_ID, + accounts: wrapper_accounts, + data: wrapper_instruction_data, + }; + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) + .await + .unwrap(); + + // Verify SPL token balance increased + let spl_account_data = rpc + .get_account(spl_token_account_keypair.pubkey()) + .await + .unwrap() + .unwrap(); + let spl_account = + spl_pod::bytemuck::pod_from_bytes::(&spl_account_data.data).unwrap(); + let final_spl_balance: u64 = spl_account.amount.into(); + assert_eq!(final_spl_balance, transfer_amount); + + // Verify Light Token balance decreased + let ctoken_account_data = rpc.get_account(ctoken_account).await.unwrap().unwrap(); + let ctoken_state = + spl_pod::bytemuck::pod_from_bytes::(&ctoken_account_data.data[..165]).unwrap(); + assert_eq!( + u64::from(ctoken_state.amount), + amount - transfer_amount, + "Light Token account balance should have decreased" + ); + + println!("Light Token to SPL invoke_signed test passed"); +} diff --git a/sdk-tests/sdk-light-token-test/Cargo.toml b/sdk-tests/sdk-light-token-test/Cargo.toml index 6a205eb6f0..b7b77d08ed 100644 --- a/sdk-tests/sdk-light-token-test/Cargo.toml +++ b/sdk-tests/sdk-light-token-test/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [lib] crate-type = ["cdylib", "lib"] -name = "native_ctoken_examples" +name = "sdk_light_token_test" doctest = false [features] 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 5352347717..300108b37c 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 @@ -7,7 +7,7 @@ use light_client::rpc::Rpc; use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_token::LIGHT_TOKEN_PROGRAM_ID; use light_token_interface::state::Token; -use native_ctoken_examples::{ApproveData, InstructionType, ID, TOKEN_ACCOUNT_SEED}; +use sdk_light_token_test::{ApproveData, InstructionType, ID, TOKEN_ACCOUNT_SEED}; use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -19,7 +19,7 @@ use solana_sdk::{ /// Test approving a delegate using ApproveCTokenCpi::invoke() #[tokio::test] async fn test_approve_invoke() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -80,7 +80,7 @@ async fn test_approve_invoke() { /// Test approving a delegate for a PDA-owned account using ApproveCTokenCpi::invoke_signed() #[tokio::test] async fn test_approve_invoke_signed() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -138,7 +138,7 @@ async fn test_approve_invoke_signed() { /// Test revoking delegation using RevokeCTokenCpi::invoke() #[tokio::test] async fn test_revoke_invoke() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -226,7 +226,7 @@ async fn test_revoke_invoke() { /// Test revoking delegation for a PDA-owned account using RevokeCTokenCpi::invoke_signed() #[tokio::test] async fn test_revoke_invoke_signed() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); 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 d80e5d02da..1eb3a70b2d 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_burn.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_burn.rs @@ -7,7 +7,7 @@ use light_client::rpc::Rpc; use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_token::LIGHT_TOKEN_PROGRAM_ID; use light_token_interface::state::Token; -use native_ctoken_examples::{BurnData, InstructionType, ID, TOKEN_ACCOUNT_SEED}; +use sdk_light_token_test::{BurnData, InstructionType, ID, TOKEN_ACCOUNT_SEED}; use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -18,7 +18,7 @@ use solana_sdk::{ /// Test burning CTokens using BurnCTokenCpi::invoke() #[tokio::test] async fn test_burn_invoke() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -81,7 +81,7 @@ async fn test_burn_invoke() { /// Test burning CTokens with PDA authority using BurnCTokenCpi::invoke_signed() #[tokio::test] async fn test_burn_invoke_signed() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); diff --git a/sdk-tests/sdk-light-token-test/tests/test_close.rs b/sdk-tests/sdk-light-token-test/tests/test_close.rs index 6a12a24dc6..98d2255c5d 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_close.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_close.rs @@ -5,7 +5,7 @@ mod shared; use light_client::rpc::Rpc; use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_token::instruction::{rent_sponsor_pda, LIGHT_TOKEN_PROGRAM_ID}; -use native_ctoken_examples::{InstructionType, ID, TOKEN_ACCOUNT_SEED}; +use sdk_light_token_test::{InstructionType, ID, TOKEN_ACCOUNT_SEED}; use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -16,7 +16,7 @@ use solana_sdk::{ /// Test closing a compressible token account using CloseCTokenAccountCpi::invoke() #[tokio::test] async fn test_close_invoke() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -70,7 +70,7 @@ async fn test_close_invoke() { /// Test closing a PDA-owned compressible token account using CloseCTokenAccountCpi::invoke_signed() #[tokio::test] async fn test_close_invoke_signed() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); 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 99f02cba97..a189792fa1 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 @@ -6,7 +6,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_client::rpc::Rpc; use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; -use native_ctoken_examples::{CreateAtaData, ATA_SEED, ID}; +use sdk_light_token_test::{CreateAtaData, ATA_SEED, ID}; use shared::setup_create_mint; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -19,7 +19,7 @@ use solana_sdk::{ async fn test_create_ata_invoke() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -93,7 +93,7 @@ async fn test_create_ata_invoke() { async fn test_create_ata_invoke_signed() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); diff --git a/sdk-tests/sdk-light-token-test/tests/test_create_mint.rs b/sdk-tests/sdk-light-token-test/tests/test_create_mint.rs index 194cbddf0b..ea63f9b1d9 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_create_mint.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_create_mint.rs @@ -15,7 +15,7 @@ use light_token_interface::{ }, state::AdditionalMetadata, }; -use native_ctoken_examples::{CreateCmintData, ID, MINT_SIGNER_SEED}; +use sdk_light_token_test::{CreateCmintData, ID, MINT_SIGNER_SEED}; use solana_sdk::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, @@ -28,7 +28,7 @@ use solana_sdk::{ async fn test_create_compressed_mint() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -154,7 +154,7 @@ async fn test_create_compressed_mint() { async fn test_create_compressed_mint_invoke_signed() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); diff --git a/sdk-tests/sdk-light-token-test/tests/test_create_token_account.rs b/sdk-tests/sdk-light-token-test/tests/test_create_token_account.rs index 3748cb5000..c7d4e8344c 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_create_token_account.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_create_token_account.rs @@ -6,7 +6,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_client::rpc::Rpc; use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; -use native_ctoken_examples::{CreateTokenAccountData, ID}; +use sdk_light_token_test::{CreateTokenAccountData, ID}; use shared::setup_create_mint; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -20,7 +20,7 @@ use solana_sdk::{ async fn test_create_token_account_invoke() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -93,7 +93,7 @@ async fn test_create_token_account_invoke() { async fn test_create_token_account_invoke_signed() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); 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 66ab66354e..480f51c80d 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 @@ -7,7 +7,7 @@ use light_client::rpc::Rpc; use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_token::LIGHT_TOKEN_PROGRAM_ID; use light_token_interface::state::Token; -use native_ctoken_examples::{InstructionType, MintToData, ID, MINT_AUTHORITY_SEED}; +use sdk_light_token_test::{InstructionType, MintToData, ID, MINT_AUTHORITY_SEED}; use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -18,7 +18,7 @@ use solana_sdk::{ /// Test minting to Light Token using CTokenMintToCpi::invoke() #[tokio::test] async fn test_ctoken_mint_to_invoke() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -89,9 +89,9 @@ async fn test_ctoken_mint_to_invoke() { async fn test_ctoken_mint_to_invoke_signed() { use light_client::indexer::Indexer; use light_token::instruction::CreateAssociatedTokenAccount; - use native_ctoken_examples::{CreateCmintData, MINT_SIGNER_SEED}; + use sdk_light_token_test::{CreateCmintData, MINT_SIGNER_SEED}; - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); 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 9bd021a5f1..e460284c13 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 @@ -7,7 +7,7 @@ use light_client::rpc::Rpc; use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_token::LIGHT_TOKEN_PROGRAM_ID; use light_token_interface::state::{AccountState, Token}; -use native_ctoken_examples::{InstructionType, FREEZE_AUTHORITY_SEED, ID}; +use sdk_light_token_test::{InstructionType, FREEZE_AUTHORITY_SEED, ID}; use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -19,7 +19,7 @@ use solana_sdk::{ /// Test freezing a Light Token account using FreezeCTokenCpi::invoke() #[tokio::test] async fn test_freeze_invoke() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -85,7 +85,7 @@ async fn test_freeze_invoke() { /// Test freezing a Light Token account with PDA freeze authority using FreezeCTokenCpi::invoke_signed() #[tokio::test] async fn test_freeze_invoke_signed() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -139,7 +139,7 @@ async fn test_freeze_invoke_signed() { /// Test thawing a frozen Light Token account using ThawCTokenCpi::invoke() #[tokio::test] async fn test_thaw_invoke() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -224,7 +224,7 @@ async fn test_thaw_invoke() { /// Test thawing a frozen Light Token account with PDA freeze authority using ThawCTokenCpi::invoke_signed() #[tokio::test] async fn test_thaw_invoke_signed() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer.rs index 7d10707e77..011494801c 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer.rs @@ -6,7 +6,7 @@ use borsh::{BorshDeserialize, BorshSerialize}; use light_client::rpc::Rpc; use light_program_test::{LightProgramTest, ProgramTestConfig}; use light_token::instruction::LIGHT_TOKEN_PROGRAM_ID; -use native_ctoken_examples::{InstructionType, TransferData, ID, TOKEN_ACCOUNT_SEED}; +use sdk_light_token_test::{InstructionType, TransferData, ID, TOKEN_ACCOUNT_SEED}; use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -17,7 +17,7 @@ use solana_sdk::{ /// Test CTokenTransfer using invoke() #[tokio::test] async fn test_ctoken_transfer_invoke() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -74,7 +74,7 @@ async fn test_ctoken_transfer_invoke() { /// Test CTokenTransfer using invoke_signed() with PDA authority #[tokio::test] async fn test_ctoken_transfer_invoke_signed() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); 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 0ff53b826a..95ed48a797 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 @@ -14,7 +14,7 @@ use light_token::{ spl_interface::{find_spl_interface_pda_with_index, CreateSplInterfacePda}, }; use light_token_interface::state::Token; -use native_ctoken_examples::{InstructionType, TransferCheckedData, ID}; +use sdk_light_token_test::{InstructionType, TransferCheckedData, ID}; use shared::*; use solana_sdk::{ instruction::{AccountMeta, Instruction}, @@ -26,7 +26,7 @@ use solana_sdk::{ /// Test transfer_checked with SPL Token mint #[tokio::test] async fn test_ctoken_transfer_checked_spl_mint() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -175,7 +175,7 @@ async fn test_ctoken_transfer_checked_spl_mint() { /// Test transfer_checked with Token-2022 mint #[tokio::test] async fn test_ctoken_transfer_checked_t22_mint() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -281,7 +281,7 @@ async fn test_ctoken_transfer_checked_t22_mint() { /// Test transfer_checked with decompressed Mint #[tokio::test] async fn test_ctoken_transfer_checked_mint() { - let config = ProgramTestConfig::new_v2(true, Some(vec![("native_ctoken_examples", ID)])); + let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_light_token_test", ID)])); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer_interface.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer_interface.rs index b2d62774a9..f01d43a4ec 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer_interface.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer_interface.rs @@ -13,7 +13,7 @@ use light_token::{ spl_interface::find_spl_interface_pda_with_index, }; use light_token_types::CPI_AUTHORITY_PDA; -use native_ctoken_examples::{TransferInterfaceData, ID, TRANSFER_INTERFACE_AUTHORITY_SEED}; +use sdk_light_token_test::{TransferInterfaceData, ID, TRANSFER_INTERFACE_AUTHORITY_SEED}; use solana_sdk::{ instruction::{AccountMeta, Instruction}, pubkey::Pubkey, @@ -30,7 +30,7 @@ use solana_sdk::{ async fn test_transfer_interface_spl_to_ctoken_invoke() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -139,7 +139,7 @@ async fn test_transfer_interface_spl_to_ctoken_invoke() { async fn test_transfer_interface_ctoken_to_spl_invoke() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -277,7 +277,7 @@ async fn test_transfer_interface_ctoken_to_spl_invoke() { async fn test_transfer_interface_ctoken_to_ctoken_invoke() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -425,7 +425,7 @@ async fn test_transfer_interface_spl_to_ctoken_invoke_signed() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -536,7 +536,7 @@ async fn test_transfer_interface_spl_to_ctoken_invoke_signed() { async fn test_transfer_interface_ctoken_to_spl_invoke_signed() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -697,7 +697,7 @@ async fn test_transfer_interface_ctoken_to_spl_invoke_signed() { async fn test_transfer_interface_ctoken_to_ctoken_invoke_signed() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -853,7 +853,7 @@ async fn test_transfer_interface_ctoken_to_ctoken_invoke_signed() { async fn test_transfer_interface_spl_to_spl_invoke() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -967,7 +967,7 @@ async fn test_transfer_interface_spl_to_spl_invoke_signed() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -1084,7 +1084,7 @@ async fn test_transfer_interface_t22_to_t22_invoke() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -1199,7 +1199,7 @@ async fn test_transfer_interface_t22_to_t22_invoke_signed() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer_spl_ctoken.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer_spl_ctoken.rs index dbf96fee7a..fbfa9fbe50 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer_spl_ctoken.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer_spl_ctoken.rs @@ -13,7 +13,7 @@ use light_token::{ spl_interface::find_spl_interface_pda_with_index, }; use light_token_types::CPI_AUTHORITY_PDA; -use native_ctoken_examples::{ +use sdk_light_token_test::{ TransferFromSplData, TransferTokenToSplData, ID, TRANSFER_AUTHORITY_SEED, }; use solana_sdk::{ @@ -28,7 +28,7 @@ use solana_sdk::{ async fn test_spl_to_ctoken_invoke() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -166,7 +166,7 @@ async fn test_spl_to_ctoken_invoke() { async fn test_ctoken_to_spl_invoke() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -333,7 +333,7 @@ async fn test_spl_to_ctoken_invoke_signed() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); @@ -452,7 +452,7 @@ async fn test_spl_to_ctoken_invoke_signed() { async fn test_ctoken_to_spl_invoke_signed() { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, - Some(vec![("native_ctoken_examples", ID)]), + Some(vec![("sdk_light_token_test", ID)]), )) .await .unwrap(); diff --git a/sdk-tests/sdk-native-test/Cargo.toml b/sdk-tests/sdk-native-test/Cargo.toml index 1b1ab3d5f2..398f4e19ec 100644 --- a/sdk-tests/sdk-native-test/Cargo.toml +++ b/sdk-tests/sdk-native-test/Cargo.toml @@ -9,7 +9,7 @@ publish = false [lib] crate-type = ["cdylib", "lib"] -name = "sdk_native_test" +name = "sdk_v1_native_test" [features] no-entrypoint = [] diff --git a/sdk-tests/sdk-native-test/tests/test.rs b/sdk-tests/sdk-native-test/tests/test.rs index f3c5e97d73..30d792487f 100644 --- a/sdk-tests/sdk-native-test/tests/test.rs +++ b/sdk-tests/sdk-native-test/tests/test.rs @@ -11,7 +11,7 @@ use light_program_test::{ use light_sdk::instruction::{ account_meta::CompressedAccountMeta, PackedAccounts, SystemAccountMetaConfig, }; -use sdk_native_test::{ +use sdk_v1_native_test::{ create_pda::CreatePdaInstructionData, update_pda::{UpdateMyCompressedAccount, UpdatePdaInstructionData}, ARRAY_LEN, @@ -24,8 +24,10 @@ use solana_sdk::{ #[tokio::test] async fn test_sdk_native_test() { - let config = - ProgramTestConfig::new_v2(true, Some(vec![("sdk_native_test", sdk_native_test::ID)])); + let config = ProgramTestConfig::new_v2( + true, + Some(vec![("sdk_v1_native_test", sdk_v1_native_test::ID)]), + ); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -36,14 +38,14 @@ async fn test_sdk_native_test() { // let (address, _) = light_sdk::address::derive_address( // &[b"compressed", &account_data], // &address_tree_info, - // &sdk_native_test::ID, + // &sdk_v1_native_test::ID, // ); // Batched trees let address_seed = hashv_to_bn254_field_size_be(&[b"compressed", account_data.as_slice()]); let address = derive_address( &address_seed, &address_tree_pubkey.to_bytes(), - &sdk_native_test::ID.to_bytes(), + &sdk_v1_native_test::ID.to_bytes(), ); let ouput_queue = rpc.get_random_state_tree_info().unwrap().queue; create_pda( @@ -81,7 +83,7 @@ pub async fn create_pda( address_tree_pubkey: Pubkey, address: [u8; 32], ) -> Result<(), RpcError> { - let system_account_meta_config = SystemAccountMetaConfig::new(sdk_native_test::ID); + let system_account_meta_config = SystemAccountMetaConfig::new(sdk_v1_native_test::ID); let mut accounts = PackedAccounts::default(); accounts.add_pre_accounts_signer(payer.pubkey()); accounts @@ -115,7 +117,7 @@ pub async fn create_pda( let inputs = instruction_data.try_to_vec().unwrap(); let instruction = Instruction { - program_id: sdk_native_test::ID, + program_id: sdk_v1_native_test::ID, accounts, data: [&[0u8][..], &inputs[..]].concat(), }; @@ -131,7 +133,7 @@ pub async fn update_pda( new_account_data: [u8; ARRAY_LEN], compressed_account: CompressedAccountWithMerkleContext, ) -> Result<(), RpcError> { - let system_account_meta_config = SystemAccountMetaConfig::new(sdk_native_test::ID); + let system_account_meta_config = SystemAccountMetaConfig::new(sdk_v1_native_test::ID); let mut accounts = PackedAccounts::default(); accounts.add_pre_accounts_signer(payer.pubkey()); accounts @@ -173,7 +175,7 @@ pub async fn update_pda( let inputs = instruction_data.try_to_vec().unwrap(); let instruction = Instruction { - program_id: sdk_native_test::ID, + program_id: sdk_v1_native_test::ID, accounts, data: [&[1u8][..], &inputs[..]].concat(), }; diff --git a/sdk-tests/sdk-token-test/src/process_create_two_mints.rs b/sdk-tests/sdk-token-test/src/process_create_two_mints.rs index 34c5f3b152..a29c7e0d8c 100644 --- a/sdk-tests/sdk-token-test/src/process_create_two_mints.rs +++ b/sdk-tests/sdk-token-test/src/process_create_two_mints.rs @@ -6,14 +6,13 @@ use light_token::{ /// Parameters for a single mint within a batch creation. /// Does not include proof since proof is shared across all mints. +/// `mint` and `compression_address` are derived internally from `mint_seed_pubkey`. #[derive(Clone, AnchorSerialize, AnchorDeserialize, Debug)] pub struct MintParams { pub decimals: u8, - pub address_merkle_tree_root_index: u16, pub mint_authority: Pubkey, - pub compression_address: [u8; 32], - pub mint: Pubkey, - pub bump: u8, + /// Optional mint bump. If `None`, derived from `find_mint_address(mint_seed_pubkey)`. + pub mint_bump: Option, pub freeze_authority: Option, pub mint_seed_pubkey: Pubkey, } @@ -32,11 +31,17 @@ pub struct CreateMintsParams { pub mints: Vec, /// Single proof covering all new addresses pub proof: CompressedProof, + /// Address merkle tree root index (shared across all mints in batch) + pub address_merkle_tree_root_index: u16, } impl CreateMintsParams { - pub fn new(mints: Vec, proof: CompressedProof) -> Self { - Self { mints, proof } + pub fn new(mints: Vec, proof: CompressedProof, root_index: u16) -> Self { + Self { + mints, + proof, + address_merkle_tree_root_index: root_index, + } } } @@ -51,11 +56,8 @@ pub fn process_create_mints<'a, 'info>( .iter() .map(|m| SingleMintParams { decimals: m.decimals, - address_merkle_tree_root_index: m.address_merkle_tree_root_index, mint_authority: solana_pubkey::Pubkey::new_from_array(m.mint_authority.to_bytes()), - compression_address: m.compression_address, - mint: solana_pubkey::Pubkey::new_from_array(m.mint.to_bytes()), - bump: m.bump, + mint_bump: m.mint_bump, freeze_authority: m .freeze_authority .map(|a| solana_pubkey::Pubkey::new_from_array(a.to_bytes())), @@ -66,7 +68,11 @@ pub fn process_create_mints<'a, 'info>( }) .collect(); - let sdk_params = SdkCreateMintsParams::new(&sdk_mints, params.proof); + let sdk_params = SdkCreateMintsParams::new( + &sdk_mints, + params.proof, + params.address_merkle_tree_root_index, + ); let payer = ctx.accounts.signer.to_account_info(); create_mints(&payer, ctx.remaining_accounts, sdk_params) diff --git a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs index e554689a60..3831cac20c 100644 --- a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs +++ b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs @@ -67,24 +67,19 @@ async fn test_create_mints(n: usize) { let mints: Vec = mint_signers .iter() - .zip(compression_addresses.iter()) - .zip(mint_pdas.iter()) .enumerate() - .map( - |(i, ((signer, compression_address), (mint_pda, bump)))| MintParams { - decimals: (6 + i) as u8, - address_merkle_tree_root_index: proof_result.addresses[i].root_index, - mint_authority: payer.pubkey(), - compression_address: *compression_address, - mint: *mint_pda, - bump: *bump, - freeze_authority: None, - mint_seed_pubkey: signer.pubkey(), - }, - ) + .map(|(i, signer)| MintParams { + decimals: (6 + i) as u8, + mint_authority: payer.pubkey(), + mint_bump: None, // derived internally + freeze_authority: None, + mint_seed_pubkey: signer.pubkey(), + }) .collect(); - let params = CreateMintsParams::new(mints, proof_result.proof.0.unwrap()); + // Root index is shared across all mints in the batch + let root_index = proof_result.addresses[0].root_index; + let params = CreateMintsParams::new(mints, proof_result.proof.0.unwrap(), root_index); let system_accounts = SystemAccounts::default(); let cpi_context_pubkey = state_tree_info diff --git a/sdk-tests/sdk-v1-native-test/Cargo.toml b/sdk-tests/sdk-v1-native-test/Cargo.toml index 29effbc096..766daf2e5c 100644 --- a/sdk-tests/sdk-v1-native-test/Cargo.toml +++ b/sdk-tests/sdk-v1-native-test/Cargo.toml @@ -9,7 +9,7 @@ publish = false [lib] crate-type = ["cdylib", "lib"] -name = "sdk_native_test" +name = "sdk_v1_native_test" [features] no-entrypoint = [] diff --git a/sdk-tests/sdk-v1-native-test/tests/test.rs b/sdk-tests/sdk-v1-native-test/tests/test.rs index 16580c9418..a93beab599 100644 --- a/sdk-tests/sdk-v1-native-test/tests/test.rs +++ b/sdk-tests/sdk-v1-native-test/tests/test.rs @@ -8,7 +8,7 @@ use light_program_test::{ use light_sdk::instruction::{ account_meta::CompressedAccountMeta, PackedAccounts, SystemAccountMetaConfig, }; -use sdk_native_test::{ +use sdk_v1_native_test::{ create_pda::CreatePdaInstructionData, update_pda::{UpdateMyCompressedAccount, UpdatePdaInstructionData}, ARRAY_LEN, @@ -21,7 +21,10 @@ use solana_sdk::{ #[tokio::test] async fn test_sdk_native_test() { - let config = ProgramTestConfig::new(true, Some(vec![("sdk_native_test", sdk_native_test::ID)])); + let config = ProgramTestConfig::new( + true, + Some(vec![("sdk_v1_native_test", sdk_v1_native_test::ID)]), + ); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -32,7 +35,7 @@ async fn test_sdk_native_test() { let (address, _) = light_sdk::address::v1::derive_address( &[b"compressed".as_slice(), account_data.as_slice()], &address_tree, - &sdk_native_test::ID, + &sdk_v1_native_test::ID, ); let v1_output_tree = rpc.test_accounts.v1_state_trees[0].merkle_tree; @@ -71,7 +74,7 @@ pub async fn create_pda( address_tree_pubkey: Pubkey, address: [u8; 32], ) -> Result<(), RpcError> { - let system_account_meta_config = SystemAccountMetaConfig::new(sdk_native_test::ID); + let system_account_meta_config = SystemAccountMetaConfig::new(sdk_v1_native_test::ID); let mut accounts = PackedAccounts::default(); accounts.add_pre_accounts_signer(payer.pubkey()); accounts @@ -105,7 +108,7 @@ pub async fn create_pda( let inputs = instruction_data.try_to_vec().unwrap(); let instruction = Instruction { - program_id: sdk_native_test::ID, + program_id: sdk_v1_native_test::ID, accounts, data: [&[0u8][..], &inputs[..]].concat(), }; @@ -121,7 +124,7 @@ pub async fn update_pda( new_account_data: [u8; ARRAY_LEN], compressed_account: CompressedAccountWithMerkleContext, ) -> Result<(), RpcError> { - let system_account_meta_config = SystemAccountMetaConfig::new(sdk_native_test::ID); + let system_account_meta_config = SystemAccountMetaConfig::new(sdk_v1_native_test::ID); let mut accounts = PackedAccounts::default(); accounts.add_pre_accounts_signer(payer.pubkey()); accounts @@ -163,7 +166,7 @@ pub async fn update_pda( let inputs = instruction_data.try_to_vec().unwrap(); let instruction = Instruction { - program_id: sdk_native_test::ID, + program_id: sdk_v1_native_test::ID, accounts, data: [&[1u8][..], &inputs[..]].concat(), }; diff --git a/sdk-tests/single-account-loader-test/Cargo.toml b/sdk-tests/single-account-loader-test/Cargo.toml index 09527aa480..d6d0064dbc 100644 --- a/sdk-tests/single-account-loader-test/Cargo.toml +++ b/sdk-tests/single-account-loader-test/Cargo.toml @@ -13,23 +13,17 @@ no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] -custom-heap = ["light-heap", "light-sdk/custom-heap"] default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +idl-build = ["anchor-lang/idl-build"] test-sbf = [] [dependencies] -light-heap = { workspace = true, optional = true } -light-sdk = { workspace = true, features = ["anchor", "v2", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } -light-sdk-macros = { workspace = true } +light-account = { workspace = true, features = ["anchor", "sha256"] } light-macros = { workspace = true, features = ["solana"] } borsh = { workspace = true } bytemuck = { workspace = true, features = ["derive"] } -light-compressible = { workspace = true, features = ["anchor"] } light-compressed-account = { workspace = true, features = ["solana"] } light-hasher = { workspace = true, features = ["solana"] } -light-token = { workspace = true, features = ["anchor"] } anchor-lang = { workspace = true } solana-program = { workspace = true } solana-pubkey = { workspace = true } @@ -41,6 +35,7 @@ solana-account-info = { workspace = true } light-program-test = { workspace = true, features = ["devenv"] } light-client = { workspace = true, features = ["v2", "anchor"] } light-test-utils = { workspace = true } +light-sdk = { workspace = true } light-token = { workspace = true } light-compressed-account = { workspace = true, features = ["solana"] } light-compressible = { workspace = true, features = ["anchor"] } diff --git a/sdk-tests/single-account-loader-test/src/lib.rs b/sdk-tests/single-account-loader-test/src/lib.rs index 337832593f..bb97974885 100644 --- a/sdk-tests/single-account-loader-test/src/lib.rs +++ b/sdk-tests/single-account-loader-test/src/lib.rs @@ -6,10 +6,9 @@ #![allow(deprecated)] use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk::derive_light_cpi_signer; -use light_sdk_macros::{light_program, LightAccounts}; -use light_sdk_types::CpiSigner; +use light_account::{ + derive_light_cpi_signer, light_program, CpiSigner, CreateAccountsProof, LightAccounts, +}; pub mod state; diff --git a/sdk-tests/single-account-loader-test/src/state.rs b/sdk-tests/single-account-loader-test/src/state.rs index d568f376a2..283752c0f1 100644 --- a/sdk-tests/single-account-loader-test/src/state.rs +++ b/sdk-tests/single-account-loader-test/src/state.rs @@ -3,8 +3,7 @@ //! Defines a Pod (zero-copy) account struct for testing AccountLoader with Light Protocol. use anchor_lang::prelude::*; -use light_sdk::{interface::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// A zero-copy account using Pod serialization. /// This account is used with AccountLoader and requires `#[light_account(init, zero_copy)]`. diff --git a/sdk-tests/single-account-loader-test/tests/test.rs b/sdk-tests/single-account-loader-test/tests/test.rs index bea7332de5..1ff1efcf7c 100644 --- a/sdk-tests/single-account-loader-test/tests/test.rs +++ b/sdk-tests/single-account-loader-test/tests/test.rs @@ -1,6 +1,7 @@ //! Integration test for single AccountLoader (zero-copy) macro validation. use anchor_lang::{InstructionData, ToAccountMetas}; +use light_account::{derive_rent_sponsor_pda, IntoVariant}; use light_client::interface::{ create_load_instructions, get_create_accounts_proof, AccountInterfaceExt, AccountSpec, CreateAccountsProofInput, InitializeRentFreeConfig, PdaSpec, @@ -10,7 +11,6 @@ use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, Indexer, ProgramTestConfig, Rpc, }; -use light_sdk::{interface::IntoVariant, utils::derive_rent_sponsor_pda}; use single_account_loader_test::{ single_account_loader_test::{LightAccountVariant, RecordSeeds}, CreateRecordParams, ZeroCopyRecord, RECORD_SEED, @@ -113,7 +113,7 @@ async fn test_create_zero_copy_record() { assert_eq!(record.counter, 0, "Record counter should be 0"); // Verify compression_info is set (state == Decompressed indicates initialized) - use light_sdk::interface::CompressionState; + use light_account::CompressionState; assert_eq!( record.compression_info.state, CompressionState::Decompressed, @@ -281,7 +281,7 @@ async fn test_zero_copy_record_full_lifecycle() { assert_eq!(record.owner, owner, "Record owner should match"); assert_eq!(record.counter, 0, "Record counter should still be 0"); // state should be Decompressed after decompression - use light_sdk::interface::CompressionState; + use light_account::CompressionState; assert_eq!( record.compression_info.state, CompressionState::Decompressed, diff --git a/sdk-tests/single-ata-test/Cargo.toml b/sdk-tests/single-ata-test/Cargo.toml index 4b8c1ae6b2..38bbd5cff5 100644 --- a/sdk-tests/single-ata-test/Cargo.toml +++ b/sdk-tests/single-ata-test/Cargo.toml @@ -13,23 +13,16 @@ no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] -custom-heap = ["light-heap", "light-sdk/custom-heap"] default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +idl-build = ["anchor-lang/idl-build"] test-sbf = [] [dependencies] -light-heap = { workspace = true, optional = true } -light-sdk = { workspace = true, features = ["anchor", "v2", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +light-account = { workspace = true, features = ["token", "anchor", "sha256"] } 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 } -light-token = { workspace = true, features = ["anchor"] } -light-token-types = { workspace = true, features = ["anchor"] } -light-compressible = { workspace = true, features = ["anchor"] } light-hasher = { workspace = true, features = ["solana"] } solana-program = { workspace = true } solana-pubkey = { workspace = true } @@ -42,6 +35,9 @@ light-program-test = { workspace = true, features = ["devenv"] } light-client = { workspace = true, features = ["v2", "anchor"] } light-test-utils = { workspace = true } light-token-interface = { workspace = true } +light-sdk = { workspace = true } +light-sdk-types = { workspace = true, features = ["token"] } +light-token = { workspace = true, features = ["anchor"] } tokio = { workspace = true } solana-sdk = { workspace = true } solana-instruction = { workspace = true } diff --git a/sdk-tests/single-ata-test/src/lib.rs b/sdk-tests/single-ata-test/src/lib.rs index e785565e4b..72bb66b1db 100644 --- a/sdk-tests/single-ata-test/src/lib.rs +++ b/sdk-tests/single-ata-test/src/lib.rs @@ -6,11 +6,10 @@ #![allow(deprecated)] use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk::derive_light_cpi_signer; -use light_sdk_macros::{light_program, LightAccounts}; -use light_sdk_types::{CpiSigner, LIGHT_TOKEN_PROGRAM_ID}; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{ + derive_light_cpi_signer, light_program, CpiSigner, CreateAccountsProof, LightAccounts, + LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_PROGRAM_ID, LIGHT_TOKEN_RENT_SPONSOR, +}; declare_id!("AtaT111111111111111111111111111111111111111"); @@ -49,7 +48,7 @@ pub struct CreateAta<'info> { pub light_token_rent_sponsor: AccountInfo<'info>, /// CHECK: Light Token Program for CPI - #[account(address = LIGHT_TOKEN_PROGRAM_ID.into())] + #[account(address = LIGHT_TOKEN_PROGRAM_ID)] pub light_token_program: AccountInfo<'info>, pub system_program: Program<'info, System>, diff --git a/sdk-tests/single-ata-test/tests/test.rs b/sdk-tests/single-ata-test/tests/test.rs index 9cfc6d3119..e5dff0f100 100644 --- a/sdk-tests/single-ata-test/tests/test.rs +++ b/sdk-tests/single-ata-test/tests/test.rs @@ -1,12 +1,12 @@ //! Integration test for single ATA macro validation. use anchor_lang::{InstructionData, ToAccountMetas}; +use light_account::derive_rent_sponsor_pda; use light_client::interface::{get_create_accounts_proof, InitializeRentFreeConfig}; use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest}, Indexer, ProgramTestConfig, Rpc, }; -use light_sdk::utils::derive_rent_sponsor_pda; use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; use solana_instruction::Instruction; diff --git a/sdk-tests/single-mint-test/Cargo.toml b/sdk-tests/single-mint-test/Cargo.toml index 79be405241..2ae322b6a1 100644 --- a/sdk-tests/single-mint-test/Cargo.toml +++ b/sdk-tests/single-mint-test/Cargo.toml @@ -13,25 +13,18 @@ no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] -custom-heap = ["light-heap", "light-sdk/custom-heap"] default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build", "light-anchor-spl/idl-build"] +idl-build = ["anchor-lang/idl-build", "light-anchor-spl/idl-build"] test-sbf = [] [dependencies] -light-heap = { workspace = true, optional = true } -light-sdk = { workspace = true, features = ["anchor", "v2", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +light-account = { workspace = true, features = ["token", "anchor", "sha256"] } light-macros = { workspace = true, features = ["solana"] } -light-sdk-macros = { workspace = true } borsh = { workspace = true } light-compressed-account = { workspace = true, features = ["solana"] } light-hasher = { workspace = true, features = ["solana"] } anchor-lang = { workspace = true } light-anchor-spl = { workspace = true, features = ["metadata"] } -light-token = { workspace = true, features = ["anchor"] } -light-token-types = { workspace = true, features = ["anchor"] } -light-compressible = { workspace = true, features = ["anchor"] } solana-program = { workspace = true } solana-pubkey = { workspace = true } solana-msg = { workspace = true } @@ -43,6 +36,10 @@ light-program-test = { workspace = true, features = ["devenv"] } light-client = { workspace = true, features = ["v2", "anchor"] } light-test-utils = { workspace = true } light-token-interface = { workspace = true } +light-sdk = { workspace = true } +light-sdk-types = { workspace = true, features = ["token"] } +light-token = { workspace = true, features = ["anchor"] } +light-token-types = { workspace = true } tokio = { workspace = true } solana-sdk = { workspace = true } solana-instruction = { workspace = true } diff --git a/sdk-tests/single-mint-test/src/lib.rs b/sdk-tests/single-mint-test/src/lib.rs index a440f42a79..674560eea1 100644 --- a/sdk-tests/single-mint-test/src/lib.rs +++ b/sdk-tests/single-mint-test/src/lib.rs @@ -6,10 +6,9 @@ #![allow(deprecated)] use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk::derive_light_cpi_signer; -use light_sdk_macros::{light_program, LightAccounts}; -use light_sdk_types::CpiSigner; +use light_account::{ + derive_light_cpi_signer, light_program, CpiSigner, CreateAccountsProof, LightAccounts, +}; declare_id!("Mint111111111111111111111111111111111111111"); diff --git a/sdk-tests/single-mint-test/tests/test.rs b/sdk-tests/single-mint-test/tests/test.rs index 47a95d42b1..ede75d0504 100644 --- a/sdk-tests/single-mint-test/tests/test.rs +++ b/sdk-tests/single-mint-test/tests/test.rs @@ -1,6 +1,7 @@ //! Integration test for single mint macro validation. use anchor_lang::{InstructionData, ToAccountMetas}; +use light_account::derive_rent_sponsor_pda; use light_client::interface::{ get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, }; @@ -8,7 +9,6 @@ use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest}, ProgramTestConfig, Rpc, }; -use light_sdk::utils::derive_rent_sponsor_pda; use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; use light_token::instruction::{find_mint_address, LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; use solana_instruction::Instruction; diff --git a/sdk-tests/single-pda-test/Cargo.toml b/sdk-tests/single-pda-test/Cargo.toml index 06e272562d..ed8ba233a6 100644 --- a/sdk-tests/single-pda-test/Cargo.toml +++ b/sdk-tests/single-pda-test/Cargo.toml @@ -13,24 +13,17 @@ no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] -custom-heap = ["light-heap", "light-sdk/custom-heap"] default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +idl-build = ["anchor-lang/idl-build"] test-sbf = [] [dependencies] -light-heap = { workspace = true, optional = true } -light-sdk = { workspace = true, features = ["anchor", "v2", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +light-account = { workspace = true, features = ["anchor", "sha256"] } 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 } -light-compressible = { workspace = true, features = ["anchor"] } light-hasher = { workspace = true, features = ["solana"] } -light-token = { workspace = true, features = ["anchor"] } -light-token-types = { workspace = true, features = ["anchor"] } solana-program = { workspace = true } solana-pubkey = { workspace = true } solana-msg = { workspace = true } @@ -41,6 +34,7 @@ solana-account-info = { workspace = true } light-program-test = { workspace = true, features = ["devenv"] } light-client = { workspace = true, features = ["v2", "anchor"] } light-test-utils = { workspace = true } +light-sdk = { workspace = true } light-token = { workspace = true } tokio = { workspace = true } solana-sdk = { workspace = true } diff --git a/sdk-tests/single-pda-test/src/instruction_accounts.rs b/sdk-tests/single-pda-test/src/instruction_accounts.rs index 5470aeef8a..298d5bba58 100644 --- a/sdk-tests/single-pda-test/src/instruction_accounts.rs +++ b/sdk-tests/single-pda-test/src/instruction_accounts.rs @@ -1,8 +1,7 @@ //! Accounts module for single-pda-test. use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk_macros::LightAccounts; +use light_account::{CreateAccountsProof, LightAccounts}; use crate::state::MinimalRecord; diff --git a/sdk-tests/single-pda-test/src/lib.rs b/sdk-tests/single-pda-test/src/lib.rs index 54a2e3618a..f3bebcfdbf 100644 --- a/sdk-tests/single-pda-test/src/lib.rs +++ b/sdk-tests/single-pda-test/src/lib.rs @@ -6,9 +6,7 @@ #![allow(deprecated)] use anchor_lang::prelude::*; -use light_sdk::derive_light_cpi_signer; -use light_sdk_macros::light_program; -use light_sdk_types::CpiSigner; +use light_account::{derive_light_cpi_signer, light_program, CpiSigner}; pub mod instruction_accounts; pub mod state; diff --git a/sdk-tests/single-pda-test/src/state.rs b/sdk-tests/single-pda-test/src/state.rs index b8d792516d..5440c331e7 100644 --- a/sdk-tests/single-pda-test/src/state.rs +++ b/sdk-tests/single-pda-test/src/state.rs @@ -1,8 +1,7 @@ //! State module for single-pda-test. use anchor_lang::prelude::*; -use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; -use light_sdk_macros::LightAccount; +use light_account::{CompressionInfo, LightAccount, LightDiscriminator}; /// Minimal record struct for testing PDA creation. /// Contains only compression_info and one field. diff --git a/sdk-tests/single-pda-test/tests/test.rs b/sdk-tests/single-pda-test/tests/test.rs index 254cef7108..79dc17d0e5 100644 --- a/sdk-tests/single-pda-test/tests/test.rs +++ b/sdk-tests/single-pda-test/tests/test.rs @@ -1,6 +1,7 @@ //! Integration test for single PDA macro validation. use anchor_lang::{InstructionData, ToAccountMetas}; +use light_account::derive_rent_sponsor_pda; use light_client::interface::{ get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, }; @@ -8,7 +9,6 @@ use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest}, ProgramTestConfig, Rpc, }; -use light_sdk::utils::derive_rent_sponsor_pda; use solana_instruction::Instruction; use solana_keypair::Keypair; use solana_pubkey::Pubkey; diff --git a/sdk-tests/single-token-test/Cargo.toml b/sdk-tests/single-token-test/Cargo.toml index b1701feead..9d283abed9 100644 --- a/sdk-tests/single-token-test/Cargo.toml +++ b/sdk-tests/single-token-test/Cargo.toml @@ -13,23 +13,16 @@ no-entrypoint = [] no-idl = [] no-log-ix-name = [] cpi = ["no-entrypoint"] -custom-heap = ["light-heap", "light-sdk/custom-heap"] default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +idl-build = ["anchor-lang/idl-build"] test-sbf = [] [dependencies] -light-heap = { workspace = true, optional = true } -light-sdk = { workspace = true, features = ["anchor", "v2", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +light-account = { workspace = true, features = ["token", "anchor", "sha256"] } 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 } -light-token = { workspace = true, features = ["anchor"] } -light-token-types = { workspace = true, features = ["anchor"] } -light-compressible = { workspace = true, features = ["anchor"] } light-hasher = { workspace = true, features = ["solana"] } solana-program = { workspace = true } solana-pubkey = { workspace = true } @@ -42,6 +35,10 @@ light-program-test = { workspace = true, features = ["devenv"] } light-client = { workspace = true, features = ["v2", "anchor"] } light-test-utils = { workspace = true } light-token-interface = { workspace = true } +light-sdk = { workspace = true } +light-sdk-types = { workspace = true, features = ["token"] } +light-token = { workspace = true, features = ["anchor"] } +light-token-types = { workspace = true } tokio = { workspace = true } solana-sdk = { workspace = true } solana-instruction = { workspace = true } diff --git a/sdk-tests/single-token-test/src/lib.rs b/sdk-tests/single-token-test/src/lib.rs index 9a14465b05..5a79f14be1 100644 --- a/sdk-tests/single-token-test/src/lib.rs +++ b/sdk-tests/single-token-test/src/lib.rs @@ -6,11 +6,10 @@ #![allow(deprecated)] use anchor_lang::prelude::*; -use light_compressible::CreateAccountsProof; -use light_sdk::derive_light_cpi_signer; -use light_sdk_macros::{light_program, LightAccounts}; -use light_sdk_types::CpiSigner; -use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; +use light_account::{ + derive_light_cpi_signer, light_program, CpiSigner, CreateAccountsProof, LightAccounts, + LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR, +}; declare_id!("TknT111111111111111111111111111111111111111"); diff --git a/sdk-tests/single-token-test/tests/test.rs b/sdk-tests/single-token-test/tests/test.rs index ecb49c02ec..485b4bba7f 100644 --- a/sdk-tests/single-token-test/tests/test.rs +++ b/sdk-tests/single-token-test/tests/test.rs @@ -1,12 +1,12 @@ //! Integration test for single token vault macro validation. use anchor_lang::{InstructionData, ToAccountMetas}; +use light_account::derive_rent_sponsor_pda; use light_client::interface::{get_create_accounts_proof, InitializeRentFreeConfig}; use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest}, Indexer, ProgramTestConfig, Rpc, }; -use light_sdk::utils::derive_rent_sponsor_pda; use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; use light_token::instruction::{LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR}; use solana_instruction::Instruction; diff --git a/xtask/src/create_compressible_config.rs b/xtask/src/create_compressible_config.rs index 63783113a1..f4e05b6020 100644 --- a/xtask/src/create_compressible_config.rs +++ b/xtask/src/create_compressible_config.rs @@ -5,7 +5,7 @@ use clap::Parser; use dirs::home_dir; use light_client::rpc::{LightClient, LightClientConfig, Rpc}; use light_compressible::{ - config::COMPRESSIBLE_CONFIG_SEED, + config::LIGHT_CONFIG_SEED, registry_instructions::CreateCompressibleConfig as CreateCompressibleConfigParams, rent::RentConfig, }; @@ -52,7 +52,7 @@ fn get_config_counter_pda() -> (Pubkey, u8) { fn get_compressible_config_pda(version: u16) -> (Pubkey, u8) { Pubkey::find_program_address( - &[COMPRESSIBLE_CONFIG_SEED, &version.to_le_bytes()], + &[LIGHT_CONFIG_SEED, &version.to_le_bytes()], ®ISTRY_PROGRAM_ID, ) }