Skip to main content

anchor_cli/fetch/
mod.rs

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;
35// Default cap on signatures pulled per history source so pagination cannot accumulate an
36// unbounded `Vec` against a program with deep history.
37const DEFAULT_MAX_SIGNATURES: usize = 1000;
38
39// Shared safety bound for any historical-IDL buffer the fetcher allocates, whether reassembling
40// compressed PMP buffer writes or holding a decompressed legacy/PMP stream. Real anchor IDL JSON
41// stays well under this limit; anything larger is rejected as malicious or corrupt.
42pub(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    // Binds a fetch tuning profile to a shared RPC client for one fetch run.
84    fn new(client: &'a RpcClient, tuning: FetchTuning) -> Self {
85        Self { client, tuning }
86    }
87
88    // Rejects slot queries that point past the cluster's current slot.
89    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    // Collects IDL chunks for a borrowed slice of signatures on the current thread.
103    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    // Chooses sequential or parallel collection for an owned signature page.
119    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    // Splits the signature list across worker threads and merges recovered chunks.
133    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
172// Extracts slot-tagged chunks for one transaction and reports per-signature failures
173// through the shared progress bar.
174fn 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
201// Parses a CLI date filter into the UTC timestamp used by signature pagination.
202fn 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// Fetches all historical IDL uploads matching the requested slot/date filters.
221#[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            // `zip` only yields `Some` when both original CLI arguments were present and parsed,
237            // so these borrowed flag values are guaranteed to exist here.
238            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    // Validate the target slot before paginating signatures
252    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    // An explicit authority means "only inspect that authority-scoped PMP metadata account".
304    // Without an authority filter we merge legacy embedded-IDL history with canonical PMP
305    // history so historical fetch stays source-agnostic for the common path.
306    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    // Merge the two sources together so the rest of the flow can treat them uniformly, preferring PMP
326    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}