Skip to main content

hyli_registry/
lib.rs

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
38/// Core upload function that sends binary bytes to the registry
39async 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
102/// Upload an ELF binary with minimal metadata (zkvm only)
103/// Reads server URL from HYLI_REGISTRY_URL env var and API key from HYLI_REGISTRY_API_KEY
104/// Additional metadata fields can be provided via the `additional_metadata` parameter
105pub 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    // Merge additional metadata if provided
125    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
171/// Download an ELF binary from the registry by program_id only (searches all contracts)
172/// Reads server URL from HYLI_REGISTRY_URL env var
173pub 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
217/// Download an ELF binary from the registry
218/// Reads server URL from HYLI_REGISTRY_URL env var and API key from HYLI_REGISTRY_API_KEY
219pub 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}