light_sdk/
account.rs

1//! # Light Account
2//!
3//! LightAccount wraps the compressed account data so that it is easy to use similar to anchor Account.
4//! LightAccount sets the discriminator and creates the compressed account data hash.
5//!
6//! Data structs used with LightAccount must implement the traits:
7//! - LightDiscriminator
8//! - BorshSerialize, BorshDeserialize
9//! - Debug, Default, Clone
10//!
11//! ### Account Data Hashing
12//!
13//! Sha256 data hashing is the recommended for most use cases.
14//! Account data is serialized into a vector with borsh and hashed with Sha256.
15//!
16//! Poseidon data hashing is recommended zk use cases.
17//! The data struct needs to implement the DataHasher implementation.
18//! The LightHasher derives, the DataHasher trait a hashing scheme from the compressed account layout.
19//! Alternatively, DataHasher can be implemented manually.
20//! Poseidon hashing is CU intensive and has limitations with regards to hash inputs see Poseidon module for details.
21//!
22//!
23//! ### Compressed account with LightDiscriminator
24//! ```
25//! use light_sdk::LightDiscriminator;
26//! use solana_pubkey::Pubkey;
27//! use borsh::{BorshSerialize, BorshDeserialize};
28//! #[derive(Clone, Debug, Default, LightDiscriminator, BorshSerialize, BorshDeserialize)]
29//! pub struct CounterAccount {
30//!     pub owner: Pubkey,
31//!     pub counter: u64,
32//! }
33//! ```
34//!
35//!
36//! ### Create compressed account
37//! ```rust
38//! # use light_sdk::{LightAccount, LightDiscriminator};
39//! # use borsh::{BorshSerialize, BorshDeserialize};
40//! # use solana_pubkey::Pubkey;
41//! #
42//! # #[derive(Clone, Debug, Default, LightDiscriminator, BorshSerialize, BorshDeserialize)]
43//! # pub struct CounterAccount {
44//! #     pub owner: Pubkey,
45//! #     pub counter: u64,
46//! # }
47//! #
48//! # let program_id = Pubkey::new_unique();
49//! # let address = [0u8; 32];
50//! # let output_tree_index = 0u8;
51//! # let owner = Pubkey::new_unique();
52//! let mut my_compressed_account = LightAccount::<CounterAccount>::new_init(
53//!     &program_id,
54//!     Some(address),
55//!     output_tree_index,
56//! );
57//! // Set data:
58//! my_compressed_account.owner = owner;
59//! ```
60//! ### Update compressed account
61//! ```rust
62//! # use light_sdk::{LightAccount, LightDiscriminator};
63//! # use light_sdk::instruction::account_meta::CompressedAccountMeta;
64//! # use borsh::{BorshSerialize, BorshDeserialize};
65//! # use solana_pubkey::Pubkey;
66//! # use solana_program_error::ProgramError;
67//! #
68//! # #[derive(Clone, Debug, Default, LightDiscriminator, BorshSerialize, BorshDeserialize)]
69//! # pub struct CounterAccount {
70//! #     pub owner: Pubkey,
71//! #     pub counter: u64,
72//! # }
73//! #
74//! # fn example() -> Result<(), ProgramError> {
75//! # let program_id = Pubkey::new_unique();
76//! # let account_meta = CompressedAccountMeta {
77//! #     output_state_tree_index: 0,
78//! #     ..Default::default()
79//! # };
80//! # let compressed_account_data = CounterAccount::default();
81//! let mut my_compressed_account = LightAccount::<CounterAccount>::new_mut(
82//!     &program_id,
83//!     &account_meta,
84//!     compressed_account_data,
85//! )?;
86//! // Increment counter.
87//! my_compressed_account.counter += 1;
88//! # Ok(())
89//! # }
90//! ```
91//! ### Close compressed account
92//! ```rust
93//! # use light_sdk::{LightAccount, LightDiscriminator};
94//! # use light_sdk::instruction::account_meta::CompressedAccountMeta;
95//! # use borsh::{BorshSerialize, BorshDeserialize};
96//! # use solana_pubkey::Pubkey;
97//! # use solana_program_error::ProgramError;
98//! #
99//! # #[derive(Clone, Debug, Default, LightDiscriminator, BorshSerialize, BorshDeserialize)]
100//! # pub struct CounterAccount {
101//! #     pub owner: Pubkey,
102//! #     pub counter: u64,
103//! # }
104//! #
105//! # fn example() -> Result<(), ProgramError> {
106//! # let program_id = Pubkey::new_unique();
107//! # let account_meta = CompressedAccountMeta {
108//! #     output_state_tree_index: 0,
109//! #     ..Default::default()
110//! # };
111//! # let compressed_account_data = CounterAccount::default();
112//! let my_compressed_account = LightAccount::<CounterAccount>::new_close(
113//!     &program_id,
114//!     &account_meta,
115//!     compressed_account_data,
116//! )?;
117//! # Ok(())
118//! # }
119//! ```
120// TODO: add example for manual hashing
121
122use std::marker::PhantomData;
123
124use light_compressed_account::{
125    compressed_account::PackedMerkleContext,
126    instruction_data::with_account_info::{CompressedAccountInfo, InAccountInfo, OutAccountInfo},
127};
128use light_sdk_types::instruction::account_meta::CompressedAccountMetaTrait;
129use solana_pubkey::Pubkey;
130
131#[cfg(feature = "poseidon")]
132use crate::light_hasher::Poseidon;
133use crate::{
134    error::LightSdkError,
135    light_hasher::{DataHasher, Hasher, Sha256},
136    AnchorDeserialize, AnchorSerialize, LightDiscriminator,
137};
138
139const DEFAULT_DATA_HASH: [u8; 32] = [0u8; 32];
140
141pub trait Size {
142    fn size(&self) -> usize;
143}
144pub use sha::LightAccount;
145/// SHA256 borsh flat hashed Light Account.
146/// This is the recommended account type for most use cases.
147pub mod sha {
148    use super::*;
149    /// Light Account variant that uses SHA256 hashing with flat borsh serialization.
150    /// This is the recommended account type for most use cases.
151    pub type LightAccount<A> = super::LightAccountInner<Sha256, A, true>;
152}
153
154/// Poseidon hashed Light Account.
155/// Poseidon hashing is zk friendly and enables you to do zk proofs over your compressed account data.
156#[cfg(feature = "poseidon")]
157pub mod poseidon {
158    use super::*;
159    /// Light Account type using Poseidon hashing.
160    /// Poseidon hashing is zk friendly and enables you to do zk proofs over your compressed account.
161    /// ### Compressed account with LightHasher and LightDiscriminator
162    /// ```rust
163    /// use light_sdk::{LightHasher, LightDiscriminator};
164    /// use solana_pubkey::Pubkey;
165    /// #[derive(Clone, Debug, Default, LightHasher, LightDiscriminator)]
166    /// pub struct CounterAccount {
167    ///     #[hash]
168    ///     pub owner: Pubkey,
169    ///     pub counter: u64,
170    /// }
171    /// ```
172    /// Constraints:
173    /// - Poseidon hashes can only take up to 12 inputs
174    ///   -> use nested structs for structs with more than 12 fields.
175    /// - Poseidon hashes inputs must be less than bn254 field size (254 bits).
176    ///   hash_to_field_size methods in light hasher can be used to hash data longer than 253 bits.
177    ///   -> use the `#[hash]` attribute for fields with data types greater than 31 bytes eg Pubkeys.
178    pub type LightAccount<A> = super::LightAccountInner<Poseidon, A, false>;
179}
180
181#[doc(hidden)]
182pub use __internal::LightAccountInner;
183
184/// INTERNAL IMPLEMENTATION - DO NOT USE DIRECTLY
185/// **Use the type aliases instead:**
186/// - `LightAccount` for Poseidon hashing
187/// - `sha::LightAccount` for SHA256 hashing
188#[doc(hidden)]
189pub mod __internal {
190    use light_compressed_account::instruction_data::{
191        data::OutputCompressedAccountWithPackedContext, with_readonly::InAccount,
192    };
193    use light_sdk_types::instruction::account_meta::CompressedAccountMetaBurn;
194    #[cfg(feature = "v2")]
195    use light_sdk_types::instruction::account_meta::CompressedAccountMetaReadOnly;
196    use solana_program_error::ProgramError;
197
198    use super::*;
199
200    #[doc(hidden)]
201    #[derive(Debug, PartialEq)]
202    pub struct LightAccountInner<
203        H: Hasher,
204        A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default,
205        const HASH_FLAT: bool,
206    > {
207        owner: Pubkey,
208        pub account: A,
209        account_info: CompressedAccountInfo,
210        should_remove_data: bool,
211        /// If set, this account is read-only and this contains the precomputed account hash.
212        pub read_only_account_hash: Option<[u8; 32]>,
213        _hasher: PhantomData<H>,
214    }
215
216    impl<
217            H: Hasher,
218            A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default,
219            const HASH_FLAT: bool,
220        > core::ops::Deref for LightAccountInner<H, A, HASH_FLAT>
221    {
222        type Target = A;
223
224        fn deref(&self) -> &Self::Target {
225            &self.account
226        }
227    }
228
229    impl<
230            H: Hasher,
231            A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default,
232            const HASH_FLAT: bool,
233        > core::ops::DerefMut for LightAccountInner<H, A, HASH_FLAT>
234    {
235        fn deref_mut(&mut self) -> &mut Self::Target {
236            assert!(
237                self.read_only_account_hash.is_none(),
238                "Cannot mutate read-only account"
239            );
240            &mut self.account
241        }
242    }
243
244    impl<
245            H: Hasher,
246            A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default,
247            const HASH_FLAT: bool,
248        > LightAccountInner<H, A, HASH_FLAT>
249    {
250        pub fn new_init(
251            owner: &impl crate::PubkeyTrait,
252            address: Option<[u8; 32]>,
253            output_state_tree_index: u8,
254        ) -> Self {
255            let output_account_info = OutAccountInfo {
256                output_merkle_tree_index: output_state_tree_index,
257                discriminator: A::LIGHT_DISCRIMINATOR,
258                ..Default::default()
259            };
260            Self {
261                owner: owner.to_solana_pubkey(),
262                account: A::default(),
263                account_info: CompressedAccountInfo {
264                    address,
265                    input: None,
266                    output: Some(output_account_info),
267                },
268                should_remove_data: false,
269                read_only_account_hash: None,
270                _hasher: PhantomData,
271            }
272        }
273
274        pub fn discriminator(&self) -> &[u8; 8] {
275            &A::LIGHT_DISCRIMINATOR
276        }
277
278        pub fn lamports(&self) -> u64 {
279            if let Some(output) = self.account_info.output.as_ref() {
280                output.lamports
281            } else if let Some(input) = self.account_info.input.as_ref() {
282                input.lamports
283            } else {
284                0
285            }
286        }
287
288        pub fn lamports_mut(&mut self) -> &mut u64 {
289            if let Some(output) = self.account_info.output.as_mut() {
290                &mut output.lamports
291            } else if let Some(input) = self.account_info.input.as_mut() {
292                &mut input.lamports
293            } else {
294                panic!("No lamports field available in account_info")
295            }
296        }
297
298        pub fn address(&self) -> &Option<[u8; 32]> {
299            &self.account_info.address
300        }
301
302        pub fn owner(&self) -> &Pubkey {
303            &self.owner
304        }
305
306        pub fn in_account_info(&self) -> &Option<InAccountInfo> {
307            &self.account_info.input
308        }
309
310        pub fn out_account_info(&mut self) -> &Option<OutAccountInfo> {
311            &self.account_info.output
312        }
313    }
314
315    // Specialized implementation for HASH_FLAT = false (structured hashing with DataHasher)
316    impl<
317            H: Hasher,
318            A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default,
319        > LightAccountInner<H, A, false>
320    {
321        pub fn new_mut(
322            owner: &impl crate::PubkeyTrait,
323            input_account_meta: &impl CompressedAccountMetaTrait,
324            input_account: A,
325        ) -> Result<Self, LightSdkError> {
326            let input_account_info = {
327                // For HASH_FLAT = false, always use DataHasher
328                let input_data_hash = input_account.hash::<H>()?;
329                let tree_info = input_account_meta.get_tree_info();
330                InAccountInfo {
331                    data_hash: input_data_hash,
332                    lamports: input_account_meta.get_lamports().unwrap_or_default(),
333                    merkle_context: PackedMerkleContext {
334                        merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
335                        queue_pubkey_index: tree_info.queue_pubkey_index,
336                        leaf_index: tree_info.leaf_index,
337                        prove_by_index: tree_info.prove_by_index,
338                    },
339                    root_index: input_account_meta.get_root_index().unwrap_or_default(),
340                    discriminator: A::LIGHT_DISCRIMINATOR,
341                }
342            };
343            let output_account_info = {
344                let output_merkle_tree_index = input_account_meta
345                    .get_output_state_tree_index()
346                    .ok_or(LightSdkError::OutputStateTreeIndexIsNone)?;
347                OutAccountInfo {
348                    lamports: input_account_meta.get_lamports().unwrap_or_default(),
349                    output_merkle_tree_index,
350                    discriminator: A::LIGHT_DISCRIMINATOR,
351                    ..Default::default()
352                }
353            };
354
355            Ok(Self {
356                owner: owner.to_solana_pubkey(),
357                account: input_account,
358                account_info: CompressedAccountInfo {
359                    address: input_account_meta.get_address(),
360                    input: Some(input_account_info),
361                    output: Some(output_account_info),
362                },
363                should_remove_data: false,
364                read_only_account_hash: None,
365                _hasher: PhantomData,
366            })
367        }
368
369        pub fn new_empty(
370            owner: &impl crate::PubkeyTrait,
371            input_account_meta: &impl CompressedAccountMetaTrait,
372        ) -> Result<Self, LightSdkError> {
373            let input_account_info = {
374                let input_data_hash = DEFAULT_DATA_HASH;
375                let tree_info = input_account_meta.get_tree_info();
376                InAccountInfo {
377                    data_hash: input_data_hash,
378                    lamports: input_account_meta.get_lamports().unwrap_or_default(),
379                    merkle_context: PackedMerkleContext {
380                        merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
381                        queue_pubkey_index: tree_info.queue_pubkey_index,
382                        leaf_index: tree_info.leaf_index,
383                        prove_by_index: tree_info.prove_by_index,
384                    },
385                    root_index: input_account_meta.get_root_index().unwrap_or_default(),
386                    discriminator: [0u8; 8],
387                }
388            };
389            let output_account_info = {
390                let output_merkle_tree_index = input_account_meta
391                    .get_output_state_tree_index()
392                    .ok_or(LightSdkError::OutputStateTreeIndexIsNone)?;
393                OutAccountInfo {
394                    lamports: input_account_meta.get_lamports().unwrap_or_default(),
395                    output_merkle_tree_index,
396                    discriminator: A::LIGHT_DISCRIMINATOR,
397                    ..Default::default()
398                }
399            };
400
401            Ok(Self {
402                owner: owner.to_solana_pubkey(),
403                account: A::default(),
404                account_info: CompressedAccountInfo {
405                    address: input_account_meta.get_address(),
406                    input: Some(input_account_info),
407                    output: Some(output_account_info),
408                },
409                should_remove_data: false,
410                read_only_account_hash: None,
411                _hasher: PhantomData,
412            })
413        }
414
415        pub fn new_close(
416            owner: &impl crate::PubkeyTrait,
417            input_account_meta: &impl CompressedAccountMetaTrait,
418            input_account: A,
419        ) -> Result<Self, LightSdkError> {
420            let mut account = Self::new_mut(owner, input_account_meta, input_account)?;
421            account.should_remove_data = true;
422
423            Ok(account)
424        }
425
426        /// Closes the compressed account.
427        /// Define whether to close the account permanently or not.
428        /// The address of an account that is closed permanently cannot be created again.
429        /// For accounts that are not closed permanently the accounts address
430        /// continues to exist in an account with discriminator and without data.
431        pub fn new_burn(
432            owner: &impl crate::PubkeyTrait,
433            input_account_meta: &CompressedAccountMetaBurn,
434            input_account: A,
435        ) -> Result<Self, LightSdkError> {
436            let input_account_info = {
437                // For HASH_FLAT = false, always use DataHasher
438                let input_data_hash = input_account.hash::<H>()?;
439                let tree_info = input_account_meta.get_tree_info();
440                InAccountInfo {
441                    data_hash: input_data_hash,
442                    lamports: input_account_meta.get_lamports().unwrap_or_default(),
443                    merkle_context: PackedMerkleContext {
444                        merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
445                        queue_pubkey_index: tree_info.queue_pubkey_index,
446                        leaf_index: tree_info.leaf_index,
447                        prove_by_index: tree_info.prove_by_index,
448                    },
449                    root_index: input_account_meta.get_root_index().unwrap_or_default(),
450                    discriminator: A::LIGHT_DISCRIMINATOR,
451                }
452            };
453
454            Ok(Self {
455                owner: owner.to_solana_pubkey(),
456                account: input_account,
457                account_info: CompressedAccountInfo {
458                    address: input_account_meta.get_address(),
459                    input: Some(input_account_info),
460                    output: None,
461                },
462                should_remove_data: false,
463                read_only_account_hash: None,
464                _hasher: PhantomData,
465            })
466        }
467
468        /// Creates a read-only compressed account for validation without state updates.
469        /// Read-only accounts are used to prove that an account exists in a specific state
470        /// without modifying it (v2 only).
471        ///
472        /// # Arguments
473        /// * `owner` - The program that owns this compressed account
474        /// * `input_account_meta` - Metadata about the existing compressed account
475        /// * `input_account` - The account data to validate
476        /// * `packed_account_pubkeys` - Slice of packed pubkeys from CPI accounts (packed accounts after system accounts)
477        ///
478        /// # Note
479        /// Data hashing is consistent with the hasher type (H): SHA256 for `sha::LightAccount`,
480        /// Poseidon for `LightAccount`. The same hasher is used for both the data hash and account hash.
481        #[cfg(feature = "v2")]
482        pub fn new_read_only(
483            owner: &impl crate::PubkeyTrait,
484            input_account_meta: &CompressedAccountMetaReadOnly,
485            input_account: A,
486            packed_account_pubkeys: &[Pubkey],
487        ) -> Result<Self, ProgramError> {
488            // Hash account data once and reuse
489            let input_data_hash = input_account
490                .hash::<H>()
491                .map_err(LightSdkError::from)
492                .map_err(ProgramError::from)?;
493            let tree_info = input_account_meta.get_tree_info();
494
495            let input_account_info = InAccountInfo {
496                data_hash: input_data_hash,
497                lamports: 0, // read-only accounts don't track lamports
498                merkle_context: PackedMerkleContext {
499                    merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
500                    queue_pubkey_index: tree_info.queue_pubkey_index,
501                    leaf_index: tree_info.leaf_index,
502                    prove_by_index: tree_info.prove_by_index,
503                },
504                root_index: input_account_meta.get_root_index().unwrap_or_default(),
505                discriminator: A::LIGHT_DISCRIMINATOR,
506            };
507
508            // Compute account hash for read-only account
509            let account_hash = {
510                use light_compressed_account::compressed_account::{
511                    CompressedAccount, CompressedAccountData,
512                };
513
514                let compressed_account = CompressedAccount {
515                    address: Some(input_account_meta.address),
516                    owner: owner.to_array().into(),
517                    data: Some(CompressedAccountData {
518                        data: vec![],               // not used for hash computation
519                        data_hash: input_data_hash, // Reuse already computed hash
520                        discriminator: A::LIGHT_DISCRIMINATOR,
521                    }),
522                    lamports: 0,
523                };
524
525                // Get merkle tree pubkey from packed pubkeys slice
526                let merkle_tree_pubkey = packed_account_pubkeys
527                    .get(tree_info.merkle_tree_pubkey_index as usize)
528                    .ok_or(LightSdkError::InvalidMerkleTreeIndex)
529                    .map_err(ProgramError::from)?
530                    .to_bytes()
531                    .into();
532
533                compressed_account
534                    .hash(&merkle_tree_pubkey, &tree_info.leaf_index, true)
535                    .map_err(LightSdkError::from)
536                    .map_err(ProgramError::from)?
537            };
538
539            Ok(Self {
540                owner: owner.to_solana_pubkey(),
541                account: input_account,
542                account_info: CompressedAccountInfo {
543                    address: Some(input_account_meta.address),
544                    input: Some(input_account_info),
545                    output: None,
546                },
547                should_remove_data: false,
548                read_only_account_hash: Some(account_hash),
549                _hasher: PhantomData,
550            })
551        }
552
553        pub fn to_account_info(mut self) -> Result<CompressedAccountInfo, ProgramError> {
554            if self.read_only_account_hash.is_some() {
555                return Err(LightSdkError::ReadOnlyAccountCannotUseToAccountInfo.into());
556            }
557
558            if let Some(output) = self.account_info.output.as_mut() {
559                if self.should_remove_data {
560                    // Data should be empty to close account.
561                    if !output.data.is_empty() {
562                        return Err(LightSdkError::ExpectedNoData.into());
563                    }
564                    output.data_hash = DEFAULT_DATA_HASH;
565                    output.discriminator = [0u8; 8];
566                } else {
567                    output.data = self
568                        .account
569                        .try_to_vec()
570                        .map_err(|_| LightSdkError::Borsh)?;
571                    // For HASH_FLAT = false, always use DataHasher
572                    output.data_hash = self
573                        .account
574                        .hash::<H>()
575                        .map_err(LightSdkError::from)
576                        .map_err(ProgramError::from)?;
577                }
578            }
579            Ok(self.account_info)
580        }
581
582        #[cfg(feature = "v2")]
583        pub fn to_packed_read_only_account(
584            self,
585        ) -> Result<
586            light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount,
587            ProgramError,
588        > {
589            let account_hash = self
590                .read_only_account_hash
591                .ok_or(LightSdkError::NotReadOnlyAccount)?;
592
593            let input_account = self
594                .account_info
595                .input
596                .ok_or(ProgramError::InvalidAccountData)?;
597
598            use light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount;
599            Ok(PackedReadOnlyCompressedAccount {
600                root_index: input_account.root_index,
601                merkle_context: input_account.merkle_context,
602                account_hash,
603            })
604        }
605        pub fn to_in_account(&self) -> Option<InAccount> {
606            self.account_info
607                .input
608                .as_ref()
609                .map(|input| input.into_in_account(self.account_info.address))
610        }
611
612        pub fn to_output_compressed_account_with_packed_context(
613            &self,
614            owner: Option<solana_pubkey::Pubkey>,
615        ) -> Result<Option<OutputCompressedAccountWithPackedContext>, ProgramError> {
616            let owner = if let Some(owner) = owner {
617                owner.to_bytes().into()
618            } else {
619                self.owner.to_bytes().into()
620            };
621
622            if let Some(mut output) = self.account_info.output.clone() {
623                if self.should_remove_data {
624                    // Data should be empty to close account.
625                    if !output.data.is_empty() {
626                        return Err(LightSdkError::ExpectedNoData.into());
627                    }
628                    output.data_hash = DEFAULT_DATA_HASH;
629                    output.discriminator = [0u8; 8];
630                } else {
631                    output.data = self
632                        .account
633                        .try_to_vec()
634                        .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
635                    // For HASH_FLAT = false, always use DataHasher
636                    output.data_hash = self
637                        .account
638                        .hash::<H>()
639                        .map_err(LightSdkError::from)
640                        .map_err(ProgramError::from)?;
641                    output.data = self
642                        .account
643                        .try_to_vec()
644                        .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
645                }
646                let result = OutputCompressedAccountWithPackedContext::from_with_owner(
647                    &output,
648                    owner,
649                    self.account_info.address,
650                );
651                Ok(Some(result))
652            } else {
653                Ok(None)
654            }
655        }
656    }
657
658    // Specialized implementation for HASH_FLAT = true (flat serialization without DataHasher)
659    impl<H: Hasher, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + Default>
660        LightAccountInner<H, A, true>
661    {
662        pub fn new_mut(
663            owner: &impl crate::PubkeyTrait,
664            input_account_meta: &impl CompressedAccountMetaTrait,
665            input_account: A,
666        ) -> Result<Self, ProgramError> {
667            let input_account_info = {
668                // For HASH_FLAT = true, use direct serialization
669                let data = input_account
670                    .try_to_vec()
671                    .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
672                let mut input_data_hash = H::hash(data.as_slice())
673                    .map_err(LightSdkError::from)
674                    .map_err(ProgramError::from)?;
675                input_data_hash[0] = 0;
676                let tree_info = input_account_meta.get_tree_info();
677                InAccountInfo {
678                    data_hash: input_data_hash,
679                    lamports: input_account_meta.get_lamports().unwrap_or_default(),
680                    merkle_context: PackedMerkleContext {
681                        merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
682                        queue_pubkey_index: tree_info.queue_pubkey_index,
683                        leaf_index: tree_info.leaf_index,
684                        prove_by_index: tree_info.prove_by_index,
685                    },
686                    root_index: input_account_meta.get_root_index().unwrap_or_default(),
687                    discriminator: A::LIGHT_DISCRIMINATOR,
688                }
689            };
690            let output_account_info = {
691                let output_merkle_tree_index = input_account_meta
692                    .get_output_state_tree_index()
693                    .ok_or(LightSdkError::OutputStateTreeIndexIsNone)
694                    .map_err(ProgramError::from)?;
695                OutAccountInfo {
696                    lamports: input_account_meta.get_lamports().unwrap_or_default(),
697                    output_merkle_tree_index,
698                    discriminator: A::LIGHT_DISCRIMINATOR,
699                    ..Default::default()
700                }
701            };
702
703            Ok(Self {
704                owner: owner.to_solana_pubkey(),
705                account: input_account,
706                account_info: CompressedAccountInfo {
707                    address: input_account_meta.get_address(),
708                    input: Some(input_account_info),
709                    output: Some(output_account_info),
710                },
711                should_remove_data: false,
712                read_only_account_hash: None,
713                _hasher: PhantomData,
714            })
715        }
716
717        // TODO: add in a different pr and release
718        // pub fn init_if_needed(
719        //     owner: &'a Pubkey,
720        //     input_account_meta: CompressedAccountMetaInitIfNeeded,
721        //     input_account: A,
722        // ) -> Result<Self, ProgramError> {
723        //     if input_account_meta.init && input_account_meta.with_new_adress {
724        //         Ok(Self::new_init(
725        //             owner,
726        //             Some(input_account_meta.address),
727        //             input_account_meta.output_state_tree_index,
728        //         ))
729        //     } else if input_account_meta.init {
730        //         // For new_empty, we need a CompressedAccountMetaTrait implementor
731        //         let tree_info = input_account_meta
732        //             .tree_info
733        //             .ok_or(LightSdkError::ExpectedTreeInfo)
734        //             .map_err(ProgramError::from)?;
735
736        //         let meta = CompressedAccountMeta {
737        //             tree_info,
738        //             address: input_account_meta.address,
739        //             output_state_tree_index: input_account_meta.output_state_tree_index,
740        //         };
741        //         Self::new_empty(owner, &meta)
742        //     } else {
743        //         // For new_mut, we need a CompressedAccountMetaTrait implementor
744        //         let tree_info = input_account_meta
745        //             .tree_info
746        //             .ok_or(LightSdkError::ExpectedTreeInfo)
747        //             .map_err(ProgramError::from)?;
748        //         let meta = CompressedAccountMeta {
749        //             tree_info,
750        //             address: input_account_meta.address,
751        //             output_state_tree_index: input_account_meta.output_state_tree_index,
752        //         };
753        //         Self::new_mut(owner, &meta, input_account)
754        //     }
755        // }
756
757        pub fn new_empty(
758            owner: &impl crate::PubkeyTrait,
759            input_account_meta: &impl CompressedAccountMetaTrait,
760        ) -> Result<Self, ProgramError> {
761            let input_account_info = {
762                let input_data_hash = DEFAULT_DATA_HASH;
763                let tree_info = input_account_meta.get_tree_info();
764                InAccountInfo {
765                    data_hash: input_data_hash,
766                    lamports: input_account_meta.get_lamports().unwrap_or_default(),
767                    merkle_context: PackedMerkleContext {
768                        merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
769                        queue_pubkey_index: tree_info.queue_pubkey_index,
770                        leaf_index: tree_info.leaf_index,
771                        prove_by_index: tree_info.prove_by_index,
772                    },
773                    root_index: input_account_meta.get_root_index().unwrap_or_default(),
774                    discriminator: [0u8; 8],
775                }
776            };
777            let output_account_info = {
778                let output_merkle_tree_index = input_account_meta
779                    .get_output_state_tree_index()
780                    .ok_or(LightSdkError::OutputStateTreeIndexIsNone)
781                    .map_err(ProgramError::from)?;
782                OutAccountInfo {
783                    lamports: input_account_meta.get_lamports().unwrap_or_default(),
784                    output_merkle_tree_index,
785                    discriminator: A::LIGHT_DISCRIMINATOR,
786                    ..Default::default()
787                }
788            };
789
790            Ok(Self {
791                owner: owner.to_solana_pubkey(),
792                account: A::default(),
793                account_info: CompressedAccountInfo {
794                    address: input_account_meta.get_address(),
795                    input: Some(input_account_info),
796                    output: Some(output_account_info),
797                },
798                should_remove_data: false,
799                read_only_account_hash: None,
800                _hasher: PhantomData,
801            })
802        }
803
804        /// Closes the compressed account.
805        /// Closed accounts can be reopened again (use LightAccount::new_empty to reopen a compressed account.).
806        /// If you want to ensure an account cannot be opened again use burn.
807        /// Closed accounts preserve the accounts address
808        /// in a compressed account without discriminator and data.
809        pub fn new_close(
810            owner: &impl crate::PubkeyTrait,
811            input_account_meta: &impl CompressedAccountMetaTrait,
812            input_account: A,
813        ) -> Result<Self, ProgramError> {
814            let mut account = Self::new_mut(owner, input_account_meta, input_account)?;
815            account.should_remove_data = true;
816            Ok(account)
817        }
818
819        /// Burns the compressed account.
820        /// The address of an account that is burned cannot be created again.
821        pub fn new_burn(
822            owner: &impl crate::PubkeyTrait,
823            input_account_meta: &CompressedAccountMetaBurn,
824            input_account: A,
825        ) -> Result<Self, ProgramError> {
826            let input_account_info = {
827                // For HASH_FLAT = true, use direct serialization
828                let data = input_account
829                    .try_to_vec()
830                    .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
831                let mut input_data_hash = H::hash(data.as_slice())
832                    .map_err(LightSdkError::from)
833                    .map_err(ProgramError::from)?;
834                input_data_hash[0] = 0;
835                let tree_info = input_account_meta.get_tree_info();
836                InAccountInfo {
837                    data_hash: input_data_hash,
838                    lamports: input_account_meta.get_lamports().unwrap_or_default(),
839                    merkle_context: PackedMerkleContext {
840                        merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
841                        queue_pubkey_index: tree_info.queue_pubkey_index,
842                        leaf_index: tree_info.leaf_index,
843                        prove_by_index: tree_info.prove_by_index,
844                    },
845                    root_index: input_account_meta.get_root_index().unwrap_or_default(),
846                    discriminator: A::LIGHT_DISCRIMINATOR,
847                }
848            };
849
850            Ok(Self {
851                owner: owner.to_solana_pubkey(),
852                account: input_account,
853                account_info: CompressedAccountInfo {
854                    address: input_account_meta.get_address(),
855                    input: Some(input_account_info),
856                    output: None,
857                },
858                should_remove_data: false,
859                read_only_account_hash: None,
860                _hasher: PhantomData,
861            })
862        }
863
864        /// Creates a read-only compressed account for validation without state updates.
865        /// Read-only accounts are used to prove that an account exists in a specific state
866        /// without modifying it (v2 only).
867        ///
868        /// # Arguments
869        /// * `owner` - The program that owns this compressed account
870        /// * `input_account_meta` - Metadata about the existing compressed account
871        /// * `input_account` - The account data to validate
872        /// * `packed_account_pubkeys` - Slice of packed pubkeys from CPI accounts (packed accounts after system accounts)
873        ///
874        /// # Note
875        /// Uses SHA256 flat hashing with borsh serialization (HASH_FLAT = true).
876        #[cfg(feature = "v2")]
877        pub fn new_read_only(
878            owner: &impl crate::PubkeyTrait,
879            input_account_meta: &CompressedAccountMetaReadOnly,
880            input_account: A,
881            packed_account_pubkeys: &[Pubkey],
882        ) -> Result<Self, ProgramError> {
883            // Hash account data once and reuse (SHA256 flat: borsh serialize then hash)
884            let data = input_account
885                .try_to_vec()
886                .map_err(|_| LightSdkError::Borsh)
887                .map_err(ProgramError::from)?;
888            let mut input_data_hash = H::hash(data.as_slice())
889                .map_err(LightSdkError::from)
890                .map_err(ProgramError::from)?;
891            input_data_hash[0] = 0;
892
893            let tree_info = input_account_meta.get_tree_info();
894
895            let input_account_info = InAccountInfo {
896                data_hash: input_data_hash,
897                lamports: 0, // read-only accounts don't track lamports
898                merkle_context: PackedMerkleContext {
899                    merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index,
900                    queue_pubkey_index: tree_info.queue_pubkey_index,
901                    leaf_index: tree_info.leaf_index,
902                    prove_by_index: tree_info.prove_by_index,
903                },
904                root_index: input_account_meta.get_root_index().unwrap_or_default(),
905                discriminator: A::LIGHT_DISCRIMINATOR,
906            };
907
908            // Compute account hash for read-only account
909            let account_hash = {
910                use light_compressed_account::compressed_account::{
911                    CompressedAccount, CompressedAccountData,
912                };
913
914                let compressed_account = CompressedAccount {
915                    address: Some(input_account_meta.address),
916                    owner: owner.to_array().into(),
917                    data: Some(CompressedAccountData {
918                        data: vec![],               // not used for hash computation
919                        data_hash: input_data_hash, // Reuse already computed hash
920                        discriminator: A::LIGHT_DISCRIMINATOR,
921                    }),
922                    lamports: 0,
923                };
924
925                // Get merkle tree pubkey from packed pubkeys slice
926                let merkle_tree_pubkey = packed_account_pubkeys
927                    .get(tree_info.merkle_tree_pubkey_index as usize)
928                    .ok_or(LightSdkError::InvalidMerkleTreeIndex)
929                    .map_err(ProgramError::from)?
930                    .to_bytes()
931                    .into();
932
933                compressed_account
934                    .hash(&merkle_tree_pubkey, &tree_info.leaf_index, true)
935                    .map_err(LightSdkError::from)
936                    .map_err(ProgramError::from)?
937            };
938
939            Ok(Self {
940                owner: owner.to_solana_pubkey(),
941                account: input_account,
942                account_info: CompressedAccountInfo {
943                    address: Some(input_account_meta.address),
944                    input: Some(input_account_info),
945                    output: None,
946                },
947                should_remove_data: false,
948                read_only_account_hash: Some(account_hash),
949                _hasher: PhantomData,
950            })
951        }
952
953        pub fn to_account_info(mut self) -> Result<CompressedAccountInfo, ProgramError> {
954            if self.read_only_account_hash.is_some() {
955                return Err(LightSdkError::ReadOnlyAccountCannotUseToAccountInfo.into());
956            }
957
958            if let Some(output) = self.account_info.output.as_mut() {
959                if self.should_remove_data {
960                    // Data should be empty to close account.
961                    if !output.data.is_empty() {
962                        return Err(LightSdkError::ExpectedNoData.into());
963                    }
964                    output.data_hash = DEFAULT_DATA_HASH;
965                    output.discriminator = [0u8; 8];
966                } else {
967                    output.data = self
968                        .account
969                        .try_to_vec()
970                        .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
971                    // For HASH_FLAT = true, use direct serialization
972                    output.data_hash = H::hash(output.data.as_slice())
973                        .map_err(LightSdkError::from)
974                        .map_err(ProgramError::from)?;
975                    output.data_hash[0] = 0;
976                }
977            }
978            Ok(self.account_info)
979        }
980
981        #[cfg(feature = "v2")]
982        pub fn to_packed_read_only_account(
983            self,
984        ) -> Result<
985            light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount,
986            ProgramError,
987        > {
988            let account_hash = self
989                .read_only_account_hash
990                .ok_or(LightSdkError::NotReadOnlyAccount)?;
991
992            let input_account = self
993                .account_info
994                .input
995                .ok_or(ProgramError::InvalidAccountData)?;
996
997            use light_compressed_account::compressed_account::PackedReadOnlyCompressedAccount;
998            Ok(PackedReadOnlyCompressedAccount {
999                root_index: input_account.root_index,
1000                merkle_context: input_account.merkle_context,
1001                account_hash,
1002            })
1003        }
1004
1005        pub fn to_in_account(&self) -> Option<InAccount> {
1006            self.account_info
1007                .input
1008                .as_ref()
1009                .map(|input| input.into_in_account(self.account_info.address))
1010        }
1011
1012        pub fn to_output_compressed_account_with_packed_context(
1013            &self,
1014            owner: Option<solana_pubkey::Pubkey>,
1015        ) -> Result<Option<OutputCompressedAccountWithPackedContext>, ProgramError> {
1016            let owner = if let Some(owner) = owner {
1017                owner.to_bytes().into()
1018            } else {
1019                self.owner.to_bytes().into()
1020            };
1021
1022            if let Some(mut output) = self.account_info.output.clone() {
1023                if self.should_remove_data {
1024                    // Data should be empty to close account.
1025                    if !output.data.is_empty() {
1026                        return Err(LightSdkError::ExpectedNoData.into());
1027                    }
1028                    output.data_hash = DEFAULT_DATA_HASH;
1029                    output.discriminator = [0u8; 8];
1030                } else {
1031                    output.data = self
1032                        .account
1033                        .try_to_vec()
1034                        .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
1035                    // For HASH_FLAT = true, use direct serialization
1036                    output.data_hash = H::hash(output.data.as_slice())
1037                        .map_err(LightSdkError::from)
1038                        .map_err(ProgramError::from)?;
1039                    output.data_hash[0] = 0;
1040                    output.data = self
1041                        .account
1042                        .try_to_vec()
1043                        .map_err(|e| ProgramError::BorshIoError(e.to_string()))?;
1044                }
1045
1046                let result = OutputCompressedAccountWithPackedContext::from_with_owner(
1047                    &output,
1048                    owner,
1049                    self.account_info.address,
1050                );
1051                Ok(Some(result))
1052            } else {
1053                Ok(None)
1054            }
1055        }
1056    }
1057}