Skip to main content

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_compressed_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address;
6use light_sdk::instruction::PackedAddressTreeInfo;
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_sdk_types::interface::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                system_accounts_offset: packed.system_accounts_offset,
132            },
133            remaining_accounts: packed.remaining_accounts,
134        });
135    }
136
137    // 1. Get address tree (opinionated: always V2)
138    let address_tree = rpc.get_address_tree_v2();
139    let address_tree_pubkey = address_tree.tree;
140
141    // 2. Derive all cold addresses
142    let derived_addresses: Vec<[u8; 32]> = inputs
143        .iter()
144        .map(|input| input.derive_address(&address_tree_pubkey, program_id))
145        .collect();
146
147    // 3. Build AddressWithTree for each
148    let addresses_with_trees: Vec<AddressWithTree> = inputs
149        .iter()
150        .zip(derived_addresses.iter())
151        .map(|(input, &address)| AddressWithTree {
152            address,
153            tree: input.address_tree(&address_tree_pubkey),
154        })
155        .collect();
156
157    // 4. Get validity proof (empty hashes = INIT flow)
158    let validity_proof = rpc
159        .get_validity_proof(vec![], addresses_with_trees, None)
160        .await?
161        .value;
162
163    // 5. Get output state tree
164    let state_tree_info = rpc
165        .get_random_state_tree_info()
166        .map_err(CreateAccountsProofError::Rpc)?;
167
168    // 6. Determine CPI context (needed for mints)
169    let has_mints = inputs
170        .iter()
171        .any(|i| matches!(i, CreateAccountsProofInput::Mint(_)));
172    let cpi_context = if has_mints {
173        state_tree_info.cpi_context
174    } else {
175        None
176    };
177
178    // 7. Pack proof (use mint-aware packing if mints are present)
179    let packed = if has_mints {
180        pack_proof_for_mints(
181            program_id,
182            validity_proof.clone(),
183            &state_tree_info,
184            cpi_context,
185        )?
186    } else {
187        pack_proof(
188            program_id,
189            validity_proof.clone(),
190            &state_tree_info,
191            cpi_context,
192        )?
193    };
194
195    // All addresses use the same tree, so just take the first packed info
196    let address_tree_info = packed
197        .packed_tree_infos
198        .address_trees
199        .first()
200        .copied()
201        .ok_or(CreateAccountsProofError::EmptyInputs)?;
202
203    Ok(CreateAccountsProofResult {
204        create_accounts_proof: CreateAccountsProof {
205            proof: validity_proof.proof,
206            address_tree_info,
207            output_state_tree_index: packed.output_tree_index,
208            state_tree_index: packed.state_tree_index,
209            system_accounts_offset: packed.system_accounts_offset,
210        },
211        remaining_accounts: packed.remaining_accounts,
212    })
213}