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#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
16pub struct BloberWithNamespace {
17 pub address: PubkeyFromStr,
19 pub namespace: String,
21}
22
23pub 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 _ => None,
97 }
98 }
99}
100
101#[derive(Debug, Clone)]
103pub struct RelevantInstructionWithAccounts {
104 pub blob: Pubkey,
105 pub blober: Pubkey,
106 pub instruction: RelevantInstruction,
107}
108
109pub 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; }
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
142pub 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 _ => None,
182 }
183 }
184}
185
186#[derive(Debug)]
188pub struct RelevantBloberInstructionWithPubkey {
189 pub blober: Pubkey,
190 pub instruction: RelevantBloberInstruction,
191}
192
193pub 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; }
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
221pub 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
240pub 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#[derive(Debug, thiserror::Error)]
254pub enum LedgerDataBlobError {
255 #[error("No declare blob instruction found")]
257 DeclareNotFound,
258 #[error("Multiple declare instructions found")]
260 MultipleDeclares,
261 #[error("Declare blob size and inserts blob size mismatch")]
263 SizeMismatch,
264 #[error("No finalize instruction found")]
266 FinalizeNotFound,
267 #[error("Multiple finalize instructions found")]
269 MultipleFinalizes,
270 #[error("Blob account not owned by the program")]
272 AccountNotOwnedByProgram,
273 #[error("Invalid PDA data: {0}")]
275 InvalidCheckpointAccount(#[from] anchor_lang::error::Error),
276}
277
278pub 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}