light_client/interface/
create_accounts_proof.rs

1//! Helper for getting validity proofs for creating new rent free accounts.
2//! Programs must pass this to light accounts that they initialize.
3
4use light_compressed_account::instruction_data::compressed_proof::ValidityProof;
5use light_sdk::instruction::PackedAddressTreeInfo;
6use light_token::compressed_token::create_compressed_mint::derive_mint_compressed_address;
7use light_token_interface::MINT_ADDRESS_TREE;
8use solana_instruction::AccountMeta;
9use solana_pubkey::Pubkey;
10use thiserror::Error;
11
12use super::pack::{pack_proof, pack_proof_for_mints, PackError};
13use crate::{
14    indexer::{AddressWithTree, Indexer, IndexerError, ValidityProofWithContext},
15    rpc::{Rpc, RpcError},
16};
17
18/// Error type for create accounts proof operations.
19#[derive(Debug, Error)]
20pub enum CreateAccountsProofError {
21    #[error("Inputs cannot be empty")]
22    EmptyInputs,
23
24    #[error("Indexer error: {0}")]
25    Indexer(#[from] IndexerError),
26
27    #[error("RPC error: {0}")]
28    Rpc(RpcError),
29
30    #[error("Pack error: {0}")]
31    Pack(#[from] PackError),
32}
33
34/// Input for creating new accounts.
35/// `program_id` from main fn is used as default owner for `Pda` variant.
36#[derive(Clone, Debug)]
37pub enum CreateAccountsProofInput {
38    /// PDA owned by the calling program (uses program_id from main fn)
39    Pda(Pubkey),
40    /// PDA with explicit owner (for cross-program accounts)
41    PdaWithOwner { pda: Pubkey, owner: Pubkey },
42    /// Mint (always uses LIGHT_TOKEN_PROGRAM_ID internally)
43    Mint(Pubkey),
44}
45
46impl CreateAccountsProofInput {
47    /// Standard PDA owned by calling program.
48    /// Address derived: `derive_address(&pda, &tree, &program_id)`
49    pub fn pda(pda: Pubkey) -> Self {
50        Self::Pda(pda)
51    }
52
53    /// PDA with explicit owner (rare: cross-program accounts).
54    /// Address derived: `derive_address(&pda, &tree, &owner)`
55    pub fn pda_with_owner(pda: Pubkey, owner: Pubkey) -> Self {
56        Self::PdaWithOwner { pda, owner }
57    }
58
59    /// Compressed mint (Mint).
60    /// Address derived: `derive_mint_compressed_address(&mint_signer, &tree)`
61    pub fn mint(mint_signer: Pubkey) -> Self {
62        Self::Mint(mint_signer)
63    }
64
65    /// Derive the cold address (mints always use MINT_ADDRESS_TREE).
66    fn derive_address(&self, address_tree: &Pubkey, program_id: &Pubkey) -> [u8; 32] {
67        match self {
68            Self::Pda(pda) => light_compressed_account::address::derive_address(
69                &pda.to_bytes(),
70                &address_tree.to_bytes(),
71                &program_id.to_bytes(),
72            ),
73            Self::PdaWithOwner { pda, owner } => light_compressed_account::address::derive_address(
74                &pda.to_bytes(),
75                &address_tree.to_bytes(),
76                &owner.to_bytes(),
77            ),
78            // Mints always use MINT_ADDRESS_TREE regardless of passed tree
79            Self::Mint(signer) => {
80                derive_mint_compressed_address(signer, &Pubkey::new_from_array(MINT_ADDRESS_TREE))
81            }
82        }
83    }
84
85    /// Get the address tree for this input type.
86    fn address_tree(&self, default_tree: &Pubkey) -> Pubkey {
87        match self {
88            Self::Pda(_) | Self::PdaWithOwner { .. } => *default_tree,
89            // Mints always use MINT_ADDRESS_TREE
90            Self::Mint(_) => Pubkey::new_from_array(MINT_ADDRESS_TREE),
91        }
92    }
93}
94
95pub use light_compressible::CreateAccountsProof;
96
97/// Result of `get_create_accounts_proof`.
98pub struct CreateAccountsProofResult {
99    /// Proof data to include in instruction data.
100    pub create_accounts_proof: CreateAccountsProof,
101    /// Remaining accounts to append to instruction accounts.
102    pub remaining_accounts: Vec<AccountMeta>,
103}
104
105/// Gets validity proof for creating new cold accounts (INIT flow).
106pub async fn get_create_accounts_proof<R: Rpc + Indexer>(
107    rpc: &R,
108    program_id: &Pubkey,
109    inputs: Vec<CreateAccountsProofInput>,
110) -> Result<CreateAccountsProofResult, CreateAccountsProofError> {
111    if inputs.is_empty() {
112        // Token-only instructions: no addresses to derive, but still need tree info
113        let state_tree_info = rpc
114            .get_random_state_tree_info()
115            .map_err(CreateAccountsProofError::Rpc)?;
116
117        // Pack system accounts with empty proof
118        let packed = pack_proof(
119            program_id,
120            ValidityProofWithContext::default(),
121            &state_tree_info,
122            None, // No CPI context needed for token-only
123        )?;
124
125        return Ok(CreateAccountsProofResult {
126            create_accounts_proof: CreateAccountsProof {
127                proof: ValidityProof::default(),
128                address_tree_info: PackedAddressTreeInfo::default(),
129                output_state_tree_index: packed.output_tree_index,
130                state_tree_index: None,
131            },
132            remaining_accounts: packed.remaining_accounts,
133        });
134    }
135
136    // 1. Get address tree (opinionated: always V2)
137    let address_tree = rpc.get_address_tree_v2();
138    let address_tree_pubkey = address_tree.tree;
139
140    // 2. Derive all cold addresses
141    let derived_addresses: Vec<[u8; 32]> = inputs
142        .iter()
143        .map(|input| input.derive_address(&address_tree_pubkey, program_id))
144        .collect();
145
146    // 3. Build AddressWithTree for each
147    let addresses_with_trees: Vec<AddressWithTree> = inputs
148        .iter()
149        .zip(derived_addresses.iter())
150        .map(|(input, &address)| AddressWithTree {
151            address,
152            tree: input.address_tree(&address_tree_pubkey),
153        })
154        .collect();
155
156    // 4. Get validity proof (empty hashes = INIT flow)
157    let validity_proof = rpc
158        .get_validity_proof(vec![], addresses_with_trees, None)
159        .await?
160        .value;
161
162    // 5. Get output state tree
163    let state_tree_info = rpc
164        .get_random_state_tree_info()
165        .map_err(CreateAccountsProofError::Rpc)?;
166
167    // 6. Determine CPI context (needed for mints)
168    let has_mints = inputs
169        .iter()
170        .any(|i| matches!(i, CreateAccountsProofInput::Mint(_)));
171    let cpi_context = if has_mints {
172        state_tree_info.cpi_context
173    } else {
174        None
175    };
176
177    // 7. Pack proof (use mint-aware packing if mints are present)
178    let packed = if has_mints {
179        pack_proof_for_mints(
180            program_id,
181            validity_proof.clone(),
182            &state_tree_info,
183            cpi_context,
184        )?
185    } else {
186        pack_proof(
187            program_id,
188            validity_proof.clone(),
189            &state_tree_info,
190            cpi_context,
191        )?
192    };
193
194    // All addresses use the same tree, so just take the first packed info
195    let address_tree_info = packed
196        .packed_tree_infos
197        .address_trees
198        .first()
199        .copied()
200        .ok_or(CreateAccountsProofError::EmptyInputs)?;
201
202    Ok(CreateAccountsProofResult {
203        create_accounts_proof: CreateAccountsProof {
204            proof: validity_proof.proof,
205            address_tree_info,
206            output_state_tree_index: packed.output_tree_index,
207            state_tree_index: packed.state_tree_index,
208        },
209        remaining_accounts: packed.remaining_accounts,
210    })
211}