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