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 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
48pub 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 _ => None,
122 }
123 }
124}
125
126#[derive(Debug, Clone)]
128pub struct RelevantInstructionWithAccounts {
129 pub blob: Pubkey,
130 pub blober: Pubkey,
131 pub instruction: RelevantInstruction,
132}
133
134pub 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; }
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
171pub 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 _ => None,
211 }
212 }
213}
214
215#[derive(Debug)]
217pub struct RelevantBloberInstructionWithPubkey {
218 pub blober: Pubkey,
219 pub instruction: RelevantBloberInstruction,
220}
221
222pub 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; }
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
253pub 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
271pub 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#[derive(Debug, thiserror::Error)]
285pub enum LedgerDataBlobError {
286 #[error("No declare blob instruction found")]
288 DeclareNotFound,
289 #[error("Multiple declare instructions found")]
291 MultipleDeclares,
292 #[error("Declare blob size and inserts blob size mismatch")]
294 SizeMismatch,
295 #[error("No finalize instruction found")]
297 FinalizeNotFound,
298 #[error("Multiple finalize instructions found")]
300 MultipleFinalizes,
301}
302
303pub 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}