data_anchor_api/
indexing.rs

1use anchor_lang::{
2    AnchorDeserialize, Discriminator, prelude::Pubkey,
3    solana_program::instruction::CompiledInstruction,
4};
5use data_anchor_blober::{
6    BLOB_ACCOUNT_INSTRUCTION_IDX, BLOB_BLOBER_INSTRUCTION_IDX, instruction::InsertChunk,
7};
8use itertools::Itertools;
9use serde::Serialize;
10use solana_transaction::versioned::VersionedTransaction;
11
12use crate::PubkeyFromStr;
13
14/// A blober PDA with an associated namespace.
15#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
16pub struct BloberWithNamespace {
17    /// The blober's public key.
18    pub address: PubkeyFromStr,
19    /// The namespace associated with the blober.
20    pub namespace: String,
21}
22
23/// A relevant [`data_anchor_blober`] instruction extracted from a [`VersionedTransaction`].
24pub enum RelevantInstruction {
25    DeclareBlob(data_anchor_blober::instruction::DeclareBlob),
26    InsertChunk(data_anchor_blober::instruction::InsertChunk),
27    FinalizeBlob(data_anchor_blober::instruction::FinalizeBlob),
28}
29
30impl std::fmt::Debug for RelevantInstruction {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            RelevantInstruction::DeclareBlob(instruction) => f
34                .debug_struct("DeclareBlob")
35                .field("size", &instruction.blob_size)
36                .field("timestamp", &instruction.timestamp)
37                .finish(),
38            RelevantInstruction::InsertChunk(instruction) => f
39                .debug_struct("InsertChunk")
40                .field("idx", &instruction.idx)
41                .finish(),
42            RelevantInstruction::FinalizeBlob(_) => f.debug_struct("FinalizeBlob").finish(),
43        }
44    }
45}
46
47impl Clone for RelevantInstruction {
48    fn clone(&self) -> Self {
49        match self {
50            RelevantInstruction::DeclareBlob(instruction) => {
51                RelevantInstruction::DeclareBlob(data_anchor_blober::instruction::DeclareBlob {
52                    blob_size: instruction.blob_size,
53                    timestamp: instruction.timestamp,
54                })
55            }
56            RelevantInstruction::InsertChunk(instruction) => {
57                RelevantInstruction::InsertChunk(data_anchor_blober::instruction::InsertChunk {
58                    idx: instruction.idx,
59                    data: instruction.data.clone(),
60                })
61            }
62            RelevantInstruction::FinalizeBlob(_) => {
63                RelevantInstruction::FinalizeBlob(data_anchor_blober::instruction::FinalizeBlob {})
64            }
65        }
66    }
67}
68
69impl RelevantInstruction {
70    pub fn try_from_slice(compiled_instruction: &CompiledInstruction) -> Option<Self> {
71        use data_anchor_blober::instruction::*;
72        let discriminator = compiled_instruction.data.get(..8)?;
73
74        match discriminator {
75            DeclareBlob::DISCRIMINATOR => {
76                let data = compiled_instruction.data.get(8..).unwrap_or_default();
77                DeclareBlob::try_from_slice(data)
78                    .map(RelevantInstruction::DeclareBlob)
79                    .ok()
80            }
81            InsertChunk::DISCRIMINATOR => {
82                let data = compiled_instruction.data.get(8..).unwrap_or_default();
83                InsertChunk::try_from_slice(data)
84                    .map(RelevantInstruction::InsertChunk)
85                    .ok()
86            }
87            FinalizeBlob::DISCRIMINATOR => {
88                let data = compiled_instruction.data.get(8..).unwrap_or_default();
89                FinalizeBlob::try_from_slice(data)
90                    .map(RelevantInstruction::FinalizeBlob)
91                    .ok()
92            }
93            // If we don't recognize the discriminator, we ignore the instruction - there might be
94            // more instructions packed into the same transaction which might not be relevant to
95            // us.
96            _ => None,
97        }
98    }
99}
100
101/// A deserialized relevant instruction, containing the blob and blober pubkeys and the instruction.
102#[derive(Debug, Clone)]
103pub struct RelevantInstructionWithAccounts {
104    pub blob: Pubkey,
105    pub blober: Pubkey,
106    pub instruction: RelevantInstruction,
107}
108
109/// Deserialize relevant instructions from a transaction, given the indices of the blob and blober
110/// accounts in the transaction.
111pub fn deserialize_relevant_instructions<'a>(
112    program_id: &Pubkey,
113    account_keys: &[Pubkey],
114    instructions: impl Iterator<Item = &'a CompiledInstruction>,
115    blob_pubkey_index: usize,
116    blober_pubkey_index: usize,
117) -> Vec<RelevantInstructionWithAccounts> {
118    instructions
119        .filter_map(|compiled_instruction| {
120            let program_id_idx: usize = compiled_instruction.program_id_index.into();
121            let relevant_program_id = account_keys.get(program_id_idx)?;
122
123            if program_id != relevant_program_id {
124                return None; // Skip instructions not related to the specified program ID.
125            }
126
127            let blob = get_account_at_index(account_keys, compiled_instruction, blob_pubkey_index)?;
128            let blober =
129                get_account_at_index(account_keys, compiled_instruction, blober_pubkey_index)?;
130            let instruction = RelevantInstruction::try_from_slice(compiled_instruction)?;
131            let relevant_instruction = RelevantInstructionWithAccounts {
132                blob,
133                blober,
134                instruction,
135            };
136
137            Some(relevant_instruction)
138        })
139        .collect()
140}
141
142/// Blober instructions that are relevant to the indexer.
143pub enum RelevantBloberInstruction {
144    Initialize(data_anchor_blober::instruction::Initialize),
145    Close(data_anchor_blober::instruction::Close),
146}
147
148impl std::fmt::Debug for RelevantBloberInstruction {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        match self {
151            RelevantBloberInstruction::Initialize(instruction) => f
152                .debug_struct("Initialize")
153                .field("trusted", &instruction.trusted)
154                .finish(),
155            RelevantBloberInstruction::Close(_) => f.debug_struct("Close").finish(),
156        }
157    }
158}
159
160impl RelevantBloberInstruction {
161    pub fn try_from_slice(compiled_instruction: &CompiledInstruction) -> Option<Self> {
162        use data_anchor_blober::instruction::*;
163        let discriminator = compiled_instruction.data.get(..8)?;
164
165        match discriminator {
166            Initialize::DISCRIMINATOR => {
167                let data = compiled_instruction.data.get(8..).unwrap_or_default();
168                Initialize::try_from_slice(data)
169                    .map(RelevantBloberInstruction::Initialize)
170                    .ok()
171            }
172            Close::DISCRIMINATOR => {
173                let data = compiled_instruction.data.get(8..).unwrap_or_default();
174                Close::try_from_slice(data)
175                    .map(RelevantBloberInstruction::Close)
176                    .ok()
177            }
178            // If we don't recognize the discriminator, we ignore the instruction - there might be
179            // more instructions packed into the same transaction which might not be relevant to
180            // us.
181            _ => None,
182        }
183    }
184}
185
186/// A deserialized relevant blober instruction, containing the blober pubkey and the instruction.
187#[derive(Debug)]
188pub struct RelevantBloberInstructionWithPubkey {
189    pub blober: Pubkey,
190    pub instruction: RelevantBloberInstruction,
191}
192
193/// Deserialize blober instructions from a transaction, returning a vector of [`RelevantBloberInstructionWithPubkey`].
194pub fn deserialize_blober_instructions<'a>(
195    program_id: &Pubkey,
196    account_keys: &[Pubkey],
197    instructions: impl Iterator<Item = &'a CompiledInstruction>,
198) -> Vec<RelevantBloberInstructionWithPubkey> {
199    instructions
200        .filter_map(|compiled_instruction| {
201            let program_id_idx: usize = compiled_instruction.program_id_index.into();
202
203            let relevant_program_id = account_keys.get(program_id_idx)?;
204
205            if program_id != relevant_program_id {
206                return None; // Skip instructions not related to the specified program ID.
207            }
208
209            let blober = get_account_at_index(account_keys, compiled_instruction, 0)?;
210
211            let instruction = RelevantBloberInstruction::try_from_slice(compiled_instruction)?;
212
213            Some(RelevantBloberInstructionWithPubkey {
214                blober,
215                instruction,
216            })
217        })
218        .collect()
219}
220
221/// Extract relevant instructions from a list of transactions.
222pub fn extract_relevant_instructions(
223    program_id: &Pubkey,
224    transactions: &[VersionedTransaction],
225) -> Vec<RelevantInstructionWithAccounts> {
226    transactions
227        .iter()
228        .flat_map(|tx| {
229            deserialize_relevant_instructions(
230                program_id,
231                tx.message.static_account_keys(),
232                tx.message.instructions().iter(),
233                BLOB_ACCOUNT_INSTRUCTION_IDX,
234                BLOB_BLOBER_INSTRUCTION_IDX,
235            )
236        })
237        .collect()
238}
239
240/// Performs the double-lookup required to find an account at a given account index in an instruction.
241/// This is required because the accounts are not stored in the instruction directly, but in a separate
242/// account list. It is computed as `payload.account_keys[instruction.accounts[index]]`.
243pub fn get_account_at_index(
244    account_keys: &[Pubkey],
245    instruction: &CompiledInstruction,
246    index: usize,
247) -> Option<Pubkey> {
248    let actual_index = *instruction.accounts.get(index)? as usize;
249    account_keys.get(actual_index).copied()
250}
251
252/// Errors that can occur when fetching blob data from the ledger.
253#[derive(Debug, thiserror::Error)]
254pub enum LedgerDataBlobError {
255    /// No declare instruction found
256    #[error("No declare blob instruction found")]
257    DeclareNotFound,
258    /// Multiple declare instructions found
259    #[error("Multiple declare instructions found")]
260    MultipleDeclares,
261    /// Declare blob size and inserts built blob size mismatch
262    #[error("Declare blob size and inserts blob size mismatch")]
263    SizeMismatch,
264    /// No finalize instruction found
265    #[error("No finalize instruction found")]
266    FinalizeNotFound,
267    /// Multiple finalize instructions found
268    #[error("Multiple finalize instructions found")]
269    MultipleFinalizes,
270    /// Checkpoint account not owned by the program
271    #[error("Blob account not owned by the program")]
272    AccountNotOwnedByProgram,
273    /// Invalid PDA data
274    #[error("Invalid PDA data: {0}")]
275    InvalidCheckpointAccount(#[from] anchor_lang::error::Error),
276}
277
278/// Extracts the blob data from the relevant instructions.
279pub fn get_blob_data_from_instructions(
280    relevant_instructions: &[RelevantInstructionWithAccounts],
281    blober: Pubkey,
282    blob: Pubkey,
283) -> Result<Vec<u8>, LedgerDataBlobError> {
284    let blob_size = relevant_instructions
285        .iter()
286        .filter_map(|instruction| {
287            if instruction.blober != blober || instruction.blob != blob {
288                return None;
289            }
290
291            match &instruction.instruction {
292                RelevantInstruction::DeclareBlob(declare) => Some(declare.blob_size),
293                _ => None,
294            }
295        })
296        .next()
297        .ok_or(LedgerDataBlobError::DeclareNotFound)?;
298
299    let inserts = relevant_instructions
300        .iter()
301        .filter_map(|instruction| {
302            if instruction.blober != blober || instruction.blob != blob {
303                return None;
304            }
305
306            let RelevantInstruction::InsertChunk(insert) = &instruction.instruction else {
307                return None;
308            };
309
310            Some(InsertChunk {
311                idx: insert.idx,
312                data: insert.data.clone(),
313            })
314        })
315        .collect::<Vec<InsertChunk>>();
316
317    let blob_data =
318        inserts
319            .iter()
320            .sorted_by_key(|insert| insert.idx)
321            .fold(Vec::new(), |mut acc, insert| {
322                acc.extend_from_slice(&insert.data);
323                acc
324            });
325
326    if blob_data.len() != blob_size as usize {
327        return Err(LedgerDataBlobError::SizeMismatch);
328    }
329
330    if !relevant_instructions.iter().any(|instruction| {
331        instruction.blober == blober
332            && instruction.blob == blob
333            && matches!(
334                instruction.instruction,
335                RelevantInstruction::FinalizeBlob(_)
336            )
337    }) {
338        return Err(LedgerDataBlobError::FinalizeNotFound);
339    }
340
341    Ok(blob_data)
342}