1use {
2 crate::config::ConfigOverride,
3 anyhow::{anyhow, Result},
4 indicatif::ProgressBar,
5 solana_pubkey::Pubkey,
6 solana_rpc_client::rpc_client::RpcClient,
7 solana_rpc_client_api::response::RpcConfirmedTransactionStatusWithSignature,
8 solana_signature::Signature,
9 std::{path::PathBuf, str::FromStr, thread},
10};
11
12mod chunks;
13mod decompress;
14mod history;
15mod legacy;
16mod output;
17mod parallel;
18mod pmp;
19mod rpc;
20mod sessions;
21
22use self::{
23 chunks::extract_chunks_from_transaction,
24 history::{merge_historical_idls, HistoricalIdlVersion, IdlHistorySource},
25 legacy::fetch_legacy_historical_idls,
26 output::{save_historical_idls, write_idl_file},
27 parallel::{historical_fetch_worker_count, should_parallelize_historical_fetch},
28 pmp::fetch_pmp_historical_idls,
29 rpc::{create_rpc_client, fetch_idl_signatures, fetch_pmp_idl_signatures},
30};
31
32const DEFAULT_MAX_RETRIES: u32 = 5;
33const DEFAULT_RETRY_BACKOFF_MS: u64 = 500;
34const PROGRESS_TICK_INTERVAL_MS: u64 = 80;
35const DEFAULT_MAX_SIGNATURES: usize = 1000;
38
39pub(super) const MAX_IDL_BUFFER_BYTES: usize = 16 * 1024 * 1024;
43
44type ChunkData = Vec<u8>;
45type SlotChunk = (u64, String, ChunkData);
46type SessionChunks = Vec<SlotChunk>;
47type ExtractedIdl = HistoricalIdlVersion;
48
49struct DecompressedSessions {
50 extracted_idls: Vec<ExtractedIdl>,
51 skipped_sessions: usize,
52}
53
54#[derive(Clone, Copy, Debug)]
55pub struct FetchTuning {
56 pub workers: Option<usize>,
57 pub no_parallel: bool,
58 pub max_retries: u32,
59 pub retry_backoff_ms: u64,
60 pub max_signatures: usize,
61 pub verbose: bool,
62}
63
64impl Default for FetchTuning {
65 fn default() -> Self {
66 Self {
67 workers: None,
68 no_parallel: false,
69 max_retries: DEFAULT_MAX_RETRIES,
70 retry_backoff_ms: DEFAULT_RETRY_BACKOFF_MS,
71 max_signatures: DEFAULT_MAX_SIGNATURES,
72 verbose: false,
73 }
74 }
75}
76
77pub struct IdlFetcher<'a> {
78 client: &'a RpcClient,
79 tuning: FetchTuning,
80}
81
82impl<'a> IdlFetcher<'a> {
83 fn new(client: &'a RpcClient, tuning: FetchTuning) -> Self {
85 Self { client, tuning }
86 }
87
88 fn validate_slot(&self, target_slot: u64) -> Result<()> {
90 let current_slot = self.client.get_slot()?;
91 if target_slot > current_slot {
92 return Err(anyhow::format_err!(
93 "Target slot {} is greater than the current slot {}. Cannot fetch IDL from a \
94 future slot.",
95 target_slot,
96 current_slot
97 ));
98 }
99 Ok(())
100 }
101
102 fn collect_chunks(
104 &self,
105 signatures: &[&RpcConfirmedTransactionStatusWithSignature],
106 pb: &ProgressBar,
107 ) -> Vec<SlotChunk> {
108 signatures
109 .iter()
110 .filter_map(|sig| {
111 pb.inc(1);
112 collect_signature_chunks(self.client, sig, &self.tuning, pb)
113 })
114 .flatten()
115 .collect()
116 }
117
118 fn collect_chunks_owned(
120 &self,
121 signatures: &[RpcConfirmedTransactionStatusWithSignature],
122 pb: &ProgressBar,
123 ) -> Vec<SlotChunk> {
124 if should_parallelize_historical_fetch(signatures.len(), &self.tuning) {
125 return self.collect_chunks_owned_parallel(signatures, pb);
126 }
127
128 let refs: Vec<&RpcConfirmedTransactionStatusWithSignature> = signatures.iter().collect();
129 self.collect_chunks(&refs, pb)
130 }
131
132 fn collect_chunks_owned_parallel(
134 &self,
135 signatures: &[RpcConfirmedTransactionStatusWithSignature],
136 pb: &ProgressBar,
137 ) -> Vec<SlotChunk> {
138 let worker_count = historical_fetch_worker_count(signatures.len(), &self.tuning);
139 if worker_count <= 1 {
140 let refs: Vec<&RpcConfirmedTransactionStatusWithSignature> =
141 signatures.iter().collect();
142 return self.collect_chunks(&refs, pb);
143 }
144
145 let chunk_size = signatures.len().div_ceil(worker_count);
146
147 thread::scope(|scope| {
148 let mut handles = Vec::new();
149
150 for signature_chunk in signatures.chunks(chunk_size) {
151 let progress = pb.clone();
152 handles.push(scope.spawn(move || {
153 signature_chunk
154 .iter()
155 .filter_map(|sig| {
156 progress.inc(1);
157 collect_signature_chunks(self.client, sig, &self.tuning, &progress)
158 })
159 .flatten()
160 .collect::<Vec<_>>()
161 }));
162 }
163
164 handles
165 .into_iter()
166 .flat_map(|handle| handle.join().expect("IDL fetch worker panicked"))
167 .collect()
168 })
169 }
170}
171
172fn collect_signature_chunks(
175 client: &RpcClient,
176 sig: &RpcConfirmedTransactionStatusWithSignature,
177 tuning: &FetchTuning,
178 pb: &ProgressBar,
179) -> Option<Vec<SlotChunk>> {
180 let signature = Signature::from_str(&sig.signature).ok()?;
181 let chunks = match extract_chunks_from_transaction(client, &signature, tuning) {
182 Ok(chunks) => chunks,
183 Err(e) => {
184 pb.println(format!("{e}"));
185 return None;
186 }
187 };
188
189 if chunks.is_empty() {
190 None
191 } else {
192 Some(
193 chunks
194 .into_iter()
195 .map(|chunk| (sig.slot, sig.signature.clone(), chunk))
196 .collect::<Vec<_>>(),
197 )
198 }
199}
200
201fn parse_date_to_timestamp(date_str: &str) -> Result<i64> {
203 use chrono::NaiveDate;
204
205 let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d").map_err(|e| {
206 anyhow!(
207 "Invalid date format '{}'. Expected YYYY-MM-DD: {}",
208 date_str,
209 e
210 )
211 })?;
212
213 let datetime = date
214 .and_hms_opt(0, 0, 0)
215 .ok_or_else(|| anyhow!("Failed to create datetime from date"))?;
216
217 Ok(datetime.and_utc().timestamp())
218}
219
220#[allow(clippy::too_many_arguments)]
222pub fn idl_fetch_historical(
223 cfg_override: &ConfigOverride,
224 address: Pubkey,
225 authority: Option<Pubkey>,
226 slot: Option<u64>,
227 before: Option<String>,
228 after: Option<String>,
229 out_dir: Option<PathBuf>,
230 tuning: FetchTuning,
231) -> Result<()> {
232 let before_timestamp = before.as_deref().map(parse_date_to_timestamp).transpose()?;
233 let after_timestamp = after.as_deref().map(parse_date_to_timestamp).transpose()?;
234 if let Some((after_ts, before_ts)) = after_timestamp.zip(before_timestamp) {
235 if after_ts > before_ts {
236 let after_value = after.as_deref().unwrap();
239 let before_value = before.as_deref().unwrap();
240 return Err(anyhow!(
241 "Invalid date range: --after ({}) must be on or before --before ({})",
242 after_value,
243 before_value,
244 ));
245 }
246 }
247
248 let client = create_rpc_client(cfg_override)?;
249 let fetcher = IdlFetcher::new(&client, tuning);
250
251 if let Some(target_slot) = slot {
253 fetcher.validate_slot(target_slot)?;
254 }
255
256 let (filter_before, filter_after) = if slot.is_some() {
257 (None, None)
258 } else {
259 (before_timestamp, after_timestamp)
260 };
261
262 let legacy_signatures = if authority.is_none() {
263 fetch_idl_signatures(
264 &client,
265 &address,
266 filter_before,
267 filter_after,
268 slot,
269 tuning.max_signatures,
270 )?
271 } else {
272 Vec::new()
273 };
274 let pmp_signatures = fetch_pmp_idl_signatures(
275 &client,
276 &address,
277 authority.as_ref(),
278 filter_before,
279 filter_after,
280 slot,
281 tuning.max_signatures,
282 )?;
283
284 if legacy_signatures.is_empty() && pmp_signatures.is_empty() {
285 if let Some(authority) = authority {
286 println!(
287 "No historical IDLs found for authority-scoped metadata account {} on program {}",
288 authority, address
289 );
290 } else {
291 println!("The program doesn't have an IDL account");
292 }
293 return Ok(());
294 }
295 if tuning.verbose {
296 println!(
297 "Found {} legacy transactions and {} PMP transactions",
298 legacy_signatures.len(),
299 pmp_signatures.len()
300 );
301 }
302
303 let mut historical_idls =
307 fetch_legacy_historical_idls(&fetcher, &legacy_signatures, tuning.verbose)?;
308 let pmp_idls = fetch_pmp_historical_idls(
309 &client,
310 &address,
311 authority.as_ref(),
312 &pmp_signatures,
313 &tuning,
314 );
315 for warning in &pmp_idls.warnings {
316 if tuning.verbose
317 || matches!(
318 warning.kind,
319 pmp::PmpHistoryWarningKind::RpcError | pmp::PmpHistoryWarningKind::MissingBuffer
320 )
321 {
322 println!("Skipping PMP slot {}: {}", warning.slot, warning.detail);
323 }
324 }
325 historical_idls = merge_historical_idls(
327 historical_idls,
328 pmp_idls
329 .idls
330 .into_iter()
331 .map(|idl| HistoricalIdlVersion {
332 slot: idl.slot,
333 signature: idl.signature,
334 source: IdlHistorySource::Pmp,
335 idl_data: idl.idl_data,
336 })
337 .collect(),
338 );
339
340 if historical_idls.is_empty() {
341 if let Some(authority) = authority {
342 println!(
343 "\nNo recoverable historical IDLs found for authority-scoped metadata account {} \
344 on program {}.",
345 authority, address
346 );
347 } else {
348 println!("\nNo IDL data could be fetched from historical slots.");
349 }
350 return Ok(());
351 }
352
353 if let Some(target_slot) = slot {
354 if let Some(selected) = historical_idls
355 .iter()
356 .find(|entry| entry.slot <= target_slot)
357 {
358 return write_idl_file(
359 &selected.idl_data,
360 &PathBuf::from(format!("idl_{}.json", target_slot)),
361 out_dir.as_deref(),
362 );
363 }
364 println!(
365 "\nNo IDL upload session or PMP metadata update found at or before slot {}.",
366 target_slot
367 );
368 return Ok(());
369 }
370
371 println!(
372 "\nSuccessfully extracted {} IDL version(s)",
373 historical_idls.len()
374 );
375 save_historical_idls(&historical_idls, out_dir)
376}