glin_contracts/
metadata_fetcher.rs1use anyhow::{Context, Result};
4use ink_metadata::InkProject;
5use std::path::Path;
6
7use glin_client::GlinClient;
8
9pub struct MetadataFetchOptions {
11 pub local_path: Option<String>,
12 pub explorer_url: Option<String>,
13 pub cache_dir: Option<String>,
14}
15
16pub async fn fetch_contract_metadata(
25 client: &GlinClient,
26 contract_address: &str,
27 options: MetadataFetchOptions,
28) -> Result<InkProject> {
29 if let Some(path) = options.local_path {
31 return load_metadata_from_file(&path);
32 }
33
34 if let Some(cache_dir) = &options.cache_dir {
36 if let Ok(metadata) = try_load_from_cache(cache_dir, contract_address) {
37 return Ok(metadata);
38 }
39 }
40
41 let code_hash_hex = match crate::chain_info::get_contract_info(client, contract_address).await {
43 Ok(info) => Some(format!("0x{}", hex::encode(info.code_hash))),
44 Err(_e) => None,
45 };
46
47 if let Some(explorer_url) = options.explorer_url {
49 match fetch_from_explorer(&explorer_url, contract_address, code_hash_hex.as_deref()).await {
50 Ok(metadata) => {
51 if let Some(cache_dir) = &options.cache_dir {
53 let _ = save_to_cache(cache_dir, contract_address, &metadata);
54 }
55
56 return Ok(metadata);
57 }
58 Err(_e) => {
59 }
61 }
62 }
63
64 Err(anyhow::anyhow!(
66 r#"Could not fetch metadata for contract {}
67
68Metadata is not stored on-chain. Please provide it using one of these methods:
69
701. Specify metadata file via local_path option
712. Use an explorer with verification via explorer_url option
723. Place metadata in cache directory: ~/.glin-forge/cache/{}.json
73
74For more info, see: https://use.ink/basics/metadata
75"#,
76 contract_address,
77 contract_address
78 ))
79}
80
81fn load_metadata_from_file(path: &str) -> Result<InkProject> {
83 let content = std::fs::read_to_string(path)
84 .with_context(|| format!("Failed to read metadata file: {}", path))?;
85
86 if path.ends_with(".contract") {
88 let bundle: serde_json::Value =
89 serde_json::from_str(&content).context("Invalid .contract bundle format")?;
90
91 let metadata: InkProject = serde_json::from_value(bundle["spec"].clone())
93 .context("Invalid metadata in .contract bundle")?;
94
95 Ok(metadata)
96 } else {
97 let metadata: InkProject =
99 serde_json::from_str(&content).context("Invalid metadata JSON format")?;
100
101 Ok(metadata)
102 }
103}
104
105async fn fetch_from_explorer(
107 explorer_url: &str,
108 contract_address: &str,
109 code_hash: Option<&str>,
110) -> Result<InkProject> {
111 let mut endpoints = vec![
113 format!(
115 "{}/api/contract/{}/metadata",
116 explorer_url, contract_address
117 ),
118 format!("{}/api/contracts/{}/abi", explorer_url, contract_address),
119 format!(
120 "{}/api/v1/contracts/{}/metadata",
121 explorer_url, contract_address
122 ),
123 ];
124
125 if let Some(hash) = code_hash {
127 endpoints.extend(vec![
128 format!("{}/api/contract/{}/metadata", explorer_url, hash),
129 format!("{}/api/contracts/metadata/{}", explorer_url, hash),
130 format!("{}/api/v1/code/{}/abi", explorer_url, hash),
131 ]);
132 }
133
134 let client = reqwest::Client::builder()
135 .timeout(std::time::Duration::from_secs(30))
136 .build()?;
137
138 for url in endpoints {
139 match client.get(&url).send().await {
140 Ok(response) if response.status().is_success() => {
141 if let Ok(metadata) = response.json::<InkProject>().await {
143 return Ok(metadata);
144 }
145 }
146 Ok(response) => {
147 eprintln!(
149 " Endpoint {} returned status: {}",
150 url,
151 response.status()
152 );
153 }
154 Err(e) => {
155 eprintln!(" Failed to connect to {}: {}", url, e);
157 }
158 }
159 }
160
161 Err(anyhow::anyhow!(
162 "No explorer endpoint returned valid metadata for contract {}",
163 contract_address
164 ))
165}
166
167fn try_load_from_cache(cache_dir: &str, contract_address: &str) -> Result<InkProject> {
169 let cache_path = Path::new(cache_dir).join(format!("{}.json", contract_address));
170
171 if !cache_path.exists() {
172 anyhow::bail!("No cache found");
173 }
174
175 load_metadata_from_file(cache_path.to_str().unwrap())
176}
177
178fn save_to_cache(cache_dir: &str, contract_address: &str, metadata: &InkProject) -> Result<()> {
180 std::fs::create_dir_all(cache_dir)?;
181
182 let cache_path = Path::new(cache_dir).join(format!("{}.json", contract_address));
183
184 let json = serde_json::to_string_pretty(metadata)?;
185 std::fs::write(cache_path, json)?;
186
187 Ok(())
188}
189
190pub fn get_default_cache_dir() -> Result<String> {
192 let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
193
194 let cache_dir = home.join(".glin-forge").join("cache");
195
196 Ok(cache_dir.to_string_lossy().to_string())
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_load_metadata_from_json() {
205 }
208
209 #[test]
210 fn test_get_default_cache_dir() {
211 let cache_dir = get_default_cache_dir();
212 assert!(cache_dir.is_ok());
213 assert!(cache_dir.unwrap().contains(".glin-forge"));
214 }
215}