data_anchor_api/
indexing.rs

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