1use std::fs;
2use std::path::Path;
3
4use anyhow::{anyhow, Context, Result};
5use serde_json::Value as JsonValue;
6
7#[derive(Debug, Clone)]
8pub struct UploadRequest<'a> {
9 pub server_url: &'a str,
10 pub api_key: &'a str,
11 pub contract: &'a str,
12 pub program_id: &'a str,
13 pub binary_path: &'a Path,
14 pub toolchain: &'a str,
15 pub commit: &'a str,
16 pub zkvm: &'a str,
17}
18
19#[derive(Debug, Clone)]
20pub struct UploadResponse {
21 pub program_id: String,
22 pub body: String,
23}
24
25pub fn program_id_hex_from_file(path: &Path) -> Result<String> {
26 let bytes = fs::read(path)
27 .with_context(|| format!("Failed to read program id file {}", path.display()))?;
28 Ok(hex::encode(bytes))
29}
30
31pub fn program_id_from_file(path: &Path) -> Result<String> {
32 let raw = fs::read(path)
33 .with_context(|| format!("Failed to read program id file {}", path.display()))?;
34 let text = String::from_utf8(raw).context("Program id file is not valid UTF-8")?;
35 Ok(text.trim().to_string())
36}
37
38async fn upload_bytes(
40 server_url: &str,
41 api_key: &str,
42 contract: &str,
43 program_id: &str,
44 binary_bytes: Vec<u8>,
45 metadata: JsonValue,
46) -> Result<UploadResponse> {
47 let binary_size = binary_bytes.len();
48 tracing::info!(
49 program_id = %program_id,
50 contract = %contract,
51 binary_size = %binary_size,
52 metadata = %metadata,
53 "Starting upload to registry"
54 );
55
56 let form = reqwest::multipart::Form::new()
57 .text("program_id", program_id.to_string())
58 .text("metadata", metadata.to_string())
59 .part(
60 "file",
61 reqwest::multipart::Part::bytes(binary_bytes)
62 .file_name("program.bin")
63 .mime_str("application/octet-stream")?,
64 );
65
66 let url = format!("{}/api/elfs/{}", server_url.trim_end_matches('/'), contract);
67 tracing::debug!(url = %url, "Sending POST request");
68
69 let client = reqwest::Client::new();
70 let response = client
71 .post(url)
72 .header("x-api-key", api_key)
73 .multipart(form)
74 .send()
75 .await
76 .context("Failed to send upload request")?;
77
78 let status = response.status();
79 if !status.is_success() {
80 let body = response.text().await.unwrap_or_default();
81 tracing::error!(
82 status = %status,
83 body = %body,
84 program_id = %program_id,
85 "Upload failed"
86 );
87 return Err(anyhow!("Upload failed: {status} {body}"));
88 }
89
90 let body = response.text().await.unwrap_or_default();
91 tracing::info!(
92 status = %status,
93 "Upload successful"
94 );
95
96 Ok(UploadResponse {
97 program_id: program_id.to_string(),
98 body,
99 })
100}
101
102pub async fn upload_elf(
106 elf_bytes: &[u8],
107 program_id: &str,
108 contract: &str,
109 zkvm: &str,
110 additional_metadata: Option<JsonValue>,
111) -> Result<UploadResponse> {
112 tracing::debug!("Reading registry configuration from environment variables");
113 let server_url = std::env::var("HYLI_REGISTRY_URL")
114 .context("HYLI_REGISTRY_URL environment variable not set")?;
115 let api_key = std::env::var("HYLI_REGISTRY_API_KEY")
116 .context("HYLI_REGISTRY_API_KEY environment variable not set")?;
117
118 tracing::debug!(server_url = %server_url, "Using registry URL from environment");
119
120 let mut metadata = serde_json::json!({
121 "zkvm": zkvm,
122 });
123
124 if let Some(additional) = additional_metadata {
126 if let (Some(base_obj), Some(add_obj)) = (metadata.as_object_mut(), additional.as_object())
127 {
128 tracing::debug!("Merging additional metadata fields");
129 for (key, value) in add_obj {
130 base_obj.insert(key.clone(), value.clone());
131 }
132 }
133 }
134
135 upload_bytes(
136 &server_url,
137 &api_key,
138 contract,
139 program_id,
140 elf_bytes.to_vec(),
141 metadata,
142 )
143 .await
144}
145
146pub async fn upload(request: UploadRequest<'_>) -> Result<UploadResponse> {
147 let binary_bytes = fs::read(request.binary_path).with_context(|| {
148 format!(
149 "Failed to read binary file {}",
150 request.binary_path.display()
151 )
152 })?;
153
154 let metadata = serde_json::json!({
155 "toolchain": request.toolchain,
156 "commit": request.commit,
157 "zkvm": request.zkvm,
158 });
159
160 upload_bytes(
161 request.server_url,
162 request.api_key,
163 request.contract,
164 request.program_id,
165 binary_bytes,
166 metadata,
167 )
168 .await
169}
170
171pub async fn download_elf_by_program_id(program_id: &str) -> Result<Vec<u8>> {
174 tracing::debug!("Reading registry configuration from environment variables");
175 let server_url = std::env::var("HYLI_REGISTRY_URL")
176 .context("HYLI_REGISTRY_URL environment variable not set")?;
177
178 tracing::info!(
179 program_id = %program_id,
180 "Downloading ELF from registry by program_id"
181 );
182
183 let url = format!(
184 "{}/api/elfs/by-program/{}",
185 server_url.trim_end_matches('/'),
186 program_id
187 );
188 tracing::debug!(url = %url, "Sending GET request");
189
190 let client = reqwest::Client::new();
191 let response = client
192 .get(&url)
193 .send()
194 .await
195 .context("Failed to send download request")?;
196
197 let status = response.status();
198 if !status.is_success() {
199 let body = response.text().await.unwrap_or_default();
200 tracing::error!(
201 status = %status,
202 body = %body,
203 program_id = %program_id,
204 "Download failed"
205 );
206 return Err(anyhow!("Download failed: {status} {body}"));
207 }
208
209 let bytes = response
210 .bytes()
211 .await
212 .context("Failed to read response body")?;
213
214 Ok(bytes.to_vec())
215}
216
217pub async fn download_elf(contract: &str, program_id: &str) -> Result<Vec<u8>> {
220 tracing::debug!("Reading registry configuration from environment variables");
221 let server_url = std::env::var("HYLI_REGISTRY_URL")
222 .context("HYLI_REGISTRY_URL environment variable not set")?;
223 let api_key = std::env::var("HYLI_REGISTRY_API_KEY")
224 .context("HYLI_REGISTRY_API_KEY environment variable not set")?;
225
226 tracing::info!(
227 program_id = %program_id,
228 contract = %contract,
229 "Downloading ELF from registry"
230 );
231
232 let url = format!(
233 "{}/api/elfs/{}/{}",
234 server_url.trim_end_matches('/'),
235 contract,
236 program_id
237 );
238 tracing::debug!(url = %url, "Sending GET request");
239
240 let client = reqwest::Client::new();
241 let response = client
242 .get(&url)
243 .header("x-api-key", &api_key)
244 .send()
245 .await
246 .context("Failed to send download request")?;
247
248 let status = response.status();
249 if !status.is_success() {
250 let body = response.text().await.unwrap_or_default();
251 tracing::error!(
252 status = %status,
253 body = %body,
254 program_id = %program_id,
255 contract = %contract,
256 "Download failed"
257 );
258 return Err(anyhow!("Download failed: {status} {body}"));
259 }
260
261 let bytes = response
262 .bytes()
263 .await
264 .context("Failed to read response body")?;
265
266 Ok(bytes.to_vec())
267}