1use std::fs;
35use std::path::PathBuf;
36use std::time::Duration;
37
38use anyhow::{bail, Context, Result};
39use base64::engine::general_purpose::STANDARD as BASE64;
40use base64::Engine;
41use serde::Deserialize;
42use ud_format::solana::{self as solana_layout, LoaderKind};
43
44pub const DEFAULT_RPC: &str = "https://api.mainnet-beta.solana.com";
48
49pub fn fetch_program_elf(program_id: &str, rpc_url: &str, use_cache: bool) -> Result<Vec<u8>> {
53 validate_pubkey(program_id)?;
54
55 let cache_dir = cache_dir()?;
60 let cache_path = cache_dir.join(format!("{program_id}.elf"));
61 if use_cache && cache_path.is_file() {
62 return fs::read(&cache_path)
63 .with_context(|| format!("read cache {}", cache_path.display()));
64 }
65
66 let account = rpc_get_account(rpc_url, program_id)
69 .with_context(|| format!("getAccountInfo {program_id}"))?;
70 let owner = account.owner.clone();
71 let data = account.decoded_data()?;
72
73 let elf = match solana_layout::classify_loader(owner.as_str()) {
74 LoaderKind::BpfLoader2 => solana_layout::strip_bpf_loader_v2(&data)
75 .with_context(|| format!("{program_id}: BPFLoader2 strip"))?
76 .to_vec(),
77 LoaderKind::Upgradeable => fetch_upgradeable_elf(rpc_url, &data, program_id)?,
78 LoaderKind::LoaderV4 => solana_layout::strip_loader_v4(&data)
79 .with_context(|| format!("{program_id}: LoaderV4 strip"))?
80 .to_vec(),
81 LoaderKind::Unknown => bail!(
82 "{program_id}: unknown loader {owner} — supported: \
83 BPFLoader2, BPFLoaderUpgradeable, LoaderV4"
84 ),
85 };
86
87 if let Err(e) = fs::create_dir_all(&cache_dir) {
89 eprintln!(
90 "warning: couldn't create cache dir {}: {e}",
91 cache_dir.display()
92 );
93 } else if let Err(e) = fs::write(&cache_path, &elf) {
94 eprintln!(
95 "warning: couldn't write cache {}: {e}",
96 cache_path.display()
97 );
98 }
99
100 Ok(elf)
101}
102
103fn fetch_upgradeable_elf(
107 rpc_url: &str,
108 program_account_data: &[u8],
109 program_id: &str,
110) -> Result<Vec<u8>> {
111 let pd_pubkey_bytes = solana_layout::programdata_pubkey(program_account_data)
112 .with_context(|| format!("{program_id}: Program account"))?;
113 let programdata_address = bs58::encode(pd_pubkey_bytes).into_string();
114
115 let pd = rpc_get_account(rpc_url, &programdata_address)
116 .with_context(|| format!("getAccountInfo (ProgramData) {programdata_address}"))?;
117 let pd_data = pd.decoded_data()?;
118
119 let stripped = solana_layout::strip_bpf_loader_upgradeable(&pd_data)
120 .with_context(|| format!("{programdata_address}: ProgramData strip"))?;
121 Ok(stripped.to_vec())
122}
123
124fn cache_dir() -> Result<PathBuf> {
125 if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
130 return Ok(PathBuf::from(xdg).join("univdreams").join("solana"));
131 }
132 if let Ok(home) = std::env::var("HOME") {
133 return Ok(PathBuf::from(home)
134 .join(".cache")
135 .join("univdreams")
136 .join("solana"));
137 }
138 if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
139 return Ok(PathBuf::from(local_app_data)
140 .join("univdreams")
141 .join("cache")
142 .join("solana"));
143 }
144 bail!(
145 "can't determine cache directory: neither $XDG_CACHE_HOME, $HOME, nor $LOCALAPPDATA is set"
146 )
147}
148
149#[derive(Debug, Deserialize)]
154struct RpcResponse<T> {
155 result: Option<T>,
156 error: Option<RpcError>,
157}
158
159#[derive(Debug, Deserialize)]
160struct RpcError {
161 code: i64,
162 message: String,
163}
164
165#[derive(Debug, Deserialize)]
166struct GetAccountInfoResult {
167 value: Option<AccountInfo>,
168}
169
170#[derive(Debug, Deserialize)]
171struct AccountInfo {
172 owner: String,
173 data: (String, String),
175 #[allow(dead_code)]
176 executable: bool,
177}
178
179impl AccountInfo {
180 fn decoded_data(&self) -> Result<Vec<u8>> {
181 let (payload, encoding) = (&self.data.0, &self.data.1);
182 if encoding != "base64" {
183 bail!("unexpected account-data encoding {encoding:?}");
184 }
185 BASE64
186 .decode(payload.as_bytes())
187 .context("base64-decode account data")
188 }
189}
190
191fn rpc_get_account(rpc_url: &str, pubkey: &str) -> Result<AccountInfo> {
192 let req = serde_json::json!({
193 "jsonrpc": "2.0",
194 "id": 1,
195 "method": "getAccountInfo",
196 "params": [
197 pubkey,
198 { "encoding": "base64" }
199 ],
200 });
201 let agent = ureq::AgentBuilder::new()
202 .timeout(Duration::from_secs(30))
203 .build();
204 let serialized = serde_json::to_string(&req).context("serialize RPC request")?;
205 let body = agent
206 .post(rpc_url)
207 .set("Content-Type", "application/json")
208 .send_string(&serialized)
209 .with_context(|| format!("POST {rpc_url}"))?
210 .into_string()
211 .context("read RPC response body")?;
212 let parsed: RpcResponse<GetAccountInfoResult> =
213 serde_json::from_str(&body).with_context(|| format!("parse RPC response: {body}"))?;
214 if let Some(err) = parsed.error {
215 bail!("RPC error {}: {}", err.code, err.message);
216 }
217 let result = parsed
218 .result
219 .ok_or_else(|| anyhow::anyhow!("RPC response missing `result`"))?;
220 result
221 .value
222 .ok_or_else(|| anyhow::anyhow!("account {pubkey} does not exist"))
223}
224
225fn validate_pubkey(s: &str) -> Result<()> {
226 let bytes = bs58::decode(s)
227 .into_vec()
228 .with_context(|| format!("decode base58 pubkey {s}"))?;
229 if bytes.len() != 32 {
230 bail!(
231 "invalid pubkey {s}: decoded to {} bytes (expected 32)",
232 bytes.len()
233 );
234 }
235 Ok(())
236}