use std::fs;
use std::path::PathBuf;
use std::time::Duration;
use anyhow::{bail, Context, Result};
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use serde::Deserialize;
use ud_format::solana::{self as solana_layout, LoaderKind};
pub const DEFAULT_RPC: &str = "https://api.mainnet-beta.solana.com";
pub fn fetch_program_elf(program_id: &str, rpc_url: &str, use_cache: bool) -> Result<Vec<u8>> {
validate_pubkey(program_id)?;
let cache_dir = cache_dir()?;
let cache_path = cache_dir.join(format!("{program_id}.elf"));
if use_cache && cache_path.is_file() {
return fs::read(&cache_path)
.with_context(|| format!("read cache {}", cache_path.display()));
}
let account = rpc_get_account(rpc_url, program_id)
.with_context(|| format!("getAccountInfo {program_id}"))?;
let owner = account.owner.clone();
let data = account.decoded_data()?;
let elf = match solana_layout::classify_loader(owner.as_str()) {
LoaderKind::BpfLoader2 => solana_layout::strip_bpf_loader_v2(&data)
.with_context(|| format!("{program_id}: BPFLoader2 strip"))?
.to_vec(),
LoaderKind::Upgradeable => fetch_upgradeable_elf(rpc_url, &data, program_id)?,
LoaderKind::LoaderV4 => solana_layout::strip_loader_v4(&data)
.with_context(|| format!("{program_id}: LoaderV4 strip"))?
.to_vec(),
LoaderKind::Unknown => bail!(
"{program_id}: unknown loader {owner} — supported: \
BPFLoader2, BPFLoaderUpgradeable, LoaderV4"
),
};
if let Err(e) = fs::create_dir_all(&cache_dir) {
eprintln!(
"warning: couldn't create cache dir {}: {e}",
cache_dir.display()
);
} else if let Err(e) = fs::write(&cache_path, &elf) {
eprintln!(
"warning: couldn't write cache {}: {e}",
cache_path.display()
);
}
Ok(elf)
}
fn fetch_upgradeable_elf(
rpc_url: &str,
program_account_data: &[u8],
program_id: &str,
) -> Result<Vec<u8>> {
let pd_pubkey_bytes = solana_layout::programdata_pubkey(program_account_data)
.with_context(|| format!("{program_id}: Program account"))?;
let programdata_address = bs58::encode(pd_pubkey_bytes).into_string();
let pd = rpc_get_account(rpc_url, &programdata_address)
.with_context(|| format!("getAccountInfo (ProgramData) {programdata_address}"))?;
let pd_data = pd.decoded_data()?;
let stripped = solana_layout::strip_bpf_loader_upgradeable(&pd_data)
.with_context(|| format!("{programdata_address}: ProgramData strip"))?;
Ok(stripped.to_vec())
}
fn cache_dir() -> Result<PathBuf> {
if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
return Ok(PathBuf::from(xdg).join("univdreams").join("solana"));
}
if let Ok(home) = std::env::var("HOME") {
return Ok(PathBuf::from(home)
.join(".cache")
.join("univdreams")
.join("solana"));
}
if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
return Ok(PathBuf::from(local_app_data)
.join("univdreams")
.join("cache")
.join("solana"));
}
bail!(
"can't determine cache directory: neither $XDG_CACHE_HOME, $HOME, nor $LOCALAPPDATA is set"
)
}
#[derive(Debug, Deserialize)]
struct RpcResponse<T> {
result: Option<T>,
error: Option<RpcError>,
}
#[derive(Debug, Deserialize)]
struct RpcError {
code: i64,
message: String,
}
#[derive(Debug, Deserialize)]
struct GetAccountInfoResult {
value: Option<AccountInfo>,
}
#[derive(Debug, Deserialize)]
struct AccountInfo {
owner: String,
data: (String, String),
#[allow(dead_code)]
executable: bool,
}
impl AccountInfo {
fn decoded_data(&self) -> Result<Vec<u8>> {
let (payload, encoding) = (&self.data.0, &self.data.1);
if encoding != "base64" {
bail!("unexpected account-data encoding {encoding:?}");
}
BASE64
.decode(payload.as_bytes())
.context("base64-decode account data")
}
}
fn rpc_get_account(rpc_url: &str, pubkey: &str) -> Result<AccountInfo> {
let req = serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "getAccountInfo",
"params": [
pubkey,
{ "encoding": "base64" }
],
});
let agent = ureq::AgentBuilder::new()
.timeout(Duration::from_secs(30))
.build();
let serialized = serde_json::to_string(&req).context("serialize RPC request")?;
let body = agent
.post(rpc_url)
.set("Content-Type", "application/json")
.send_string(&serialized)
.with_context(|| format!("POST {rpc_url}"))?
.into_string()
.context("read RPC response body")?;
let parsed: RpcResponse<GetAccountInfoResult> =
serde_json::from_str(&body).with_context(|| format!("parse RPC response: {body}"))?;
if let Some(err) = parsed.error {
bail!("RPC error {}: {}", err.code, err.message);
}
let result = parsed
.result
.ok_or_else(|| anyhow::anyhow!("RPC response missing `result`"))?;
result
.value
.ok_or_else(|| anyhow::anyhow!("account {pubkey} does not exist"))
}
fn validate_pubkey(s: &str) -> Result<()> {
let bytes = bs58::decode(s)
.into_vec()
.with_context(|| format!("decode base58 pubkey {s}"))?;
if bytes.len() != 32 {
bail!(
"invalid pubkey {s}: decoded to {} bytes (expected 32)",
bytes.len()
);
}
Ok(())
}