solana-program-dumper 0.1.0

Dump a Solana program's executable (.so) and every account it owns, in one command.
use std::{collections::HashSet, process::Command};

use solana_account_decoder_client_types::UiAccountEncoding;
use solana_client::{
    rpc_client::RpcClient,
    rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
    rpc_filter::RpcFilterType,
};

use crate::cli::Args;

type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;

/// Shell out to `solana program dump` for the executable.
pub fn dump_program(args: &Args) -> Result<()> {
    let out_path = args.dir.join("program.so");

    let output = Command::new("solana")
        .arg("program")
        .arg("dump")
        .arg(args.program.to_string())
        .arg(&out_path)
        .arg("--url")
        .arg(&args.url)
        .arg("--commitment")
        .arg(args.commitment.as_str())
        .output()
        .map_err(|e| match e.kind() {
            std::io::ErrorKind::NotFound => Box::<dyn std::error::Error>::from(
                "the `solana` CLI is required but was not found on your PATH. \
                 Install it from https://solana.com/docs/intro/installation",
            ),
            _ => format!("could not run `solana`: {e}").into(),
        })?;

    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr);
        return Err(format!("`solana program dump` failed: {}", stderr.trim()).into());
    }

    println!(
        "Wrote executable to {} ({} bytes)",
        out_path.display(),
        std::fs::metadata(&out_path).map(|m| m.len()).unwrap_or(0)
    );
    Ok(())
}

/// Fetch every account owned by the program and write each to its own file.
pub fn dump_accounts(args: &Args) -> Result<()> {
    let client = RpcClient::new_with_timeout_and_commitment(
        args.url.clone(),
        args.timeout(),
        args.commitment.config(),
    );

    let config = RpcProgramAccountsConfig {
        // A dataSize filter is applied by the node, so this is the difference
        // between a viable request and a timeout on a program with many
        // accounts. --skip-size cannot be expressed here: gPA filters are
        // inclusive, and there is no "not this size".
        filters: args.only_size.map(|n| vec![RpcFilterType::DataSize(n)]),
        account_config: RpcAccountInfoConfig {
            // Must be a binary encoding: solana-client panics decoding the
            // response otherwise.
            encoding: Some(UiAccountEncoding::Base64Zstd),
            commitment: Some(args.commitment.config()),
            ..Default::default()
        },
        ..Default::default()
    };

    let accounts = client
        .get_program_ui_accounts_with_config(&args.program, config)
        .map_err(|e| {
            format!(
                "{e}\n\nhint: many public RPC endpoints restrict or disable \
                 getProgramAccounts. A dedicated RPC provider, or --only-size to \
                 narrow the query, usually resolves this."
            )
        })?;

    println!("Found {} program accounts", accounts.len());

    let skip: HashSet<usize> = args.skip_size.iter().copied().collect();
    let mut written = 0usize;
    let mut skipped = 0usize;
    let mut failed = 0usize;

    for (address, ui_account) in accounts {
        let Some(account) = ui_account.to_account() else {
            eprintln!("error: could not decode account data for {address}");
            failed += 1;
            continue;
        };
        if skip.contains(&account.data.len()) {
            skipped += 1;
            continue;
        }
        let path = args
            .dir
            .join(format!("{}_{address}.bin", account.data.len()));
        match std::fs::write(&path, &account.data) {
            Ok(()) => written += 1,
            Err(e) => {
                eprintln!("error: could not write {}: {e}", path.display());
                failed += 1;
            }
        }
    }

    println!(
        "Wrote {written} accounts to {}/<data_len>_<address>.bin",
        args.dir.display()
    );
    if skipped > 0 {
        println!("Skipped {skipped} accounts matching --skip-size");
    }
    if failed > 0 {
        return Err(format!("{failed} accounts could not be written").into());
    }
    Ok(())
}