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) => {
44 Some(format!("0x{}", hex::encode(&info.code_hash)))
45 }
46 Err(_e) => {
47 None
48 }
49 };
50
51 if let Some(explorer_url) = options.explorer_url {
53 match fetch_from_explorer(&explorer_url, contract_address, code_hash_hex.as_deref()).await {
54 Ok(metadata) => {
55 if let Some(cache_dir) = &options.cache_dir {
57 let _ = save_to_cache(cache_dir, contract_address, &metadata);
58 }
59
60 return Ok(metadata);
61 }
62 Err(_e) => {
63 }
65 }
66 }
67
68 Err(anyhow::anyhow!(
70 r#"Could not fetch metadata for contract {}
71
72Metadata is not stored on-chain. Please provide it using one of these methods:
73
741. Specify metadata file via local_path option
752. Use an explorer with verification via explorer_url option
763. Place metadata in cache directory: ~/.glin-forge/cache/{}.json
77
78For more info, see: https://use.ink/basics/metadata
79"#,
80 contract_address,
81 contract_address
82 ))
83}
84
85fn load_metadata_from_file(path: &str) -> Result<InkProject> {
87 let content = std::fs::read_to_string(path)
88 .with_context(|| format!("Failed to read metadata file: {}", path))?;
89
90 if path.ends_with(".contract") {
92 let bundle: serde_json::Value = serde_json::from_str(&content)
93 .context("Invalid .contract bundle format")?;
94
95 let metadata: InkProject = serde_json::from_value(bundle["spec"].clone())
97 .context("Invalid metadata in .contract bundle")?;
98
99 Ok(metadata)
100 } else {
101 let metadata: InkProject =
103 serde_json::from_str(&content).context("Invalid metadata JSON format")?;
104
105 Ok(metadata)
106 }
107}
108
109async fn fetch_from_explorer(
111 explorer_url: &str,
112 contract_address: &str,
113 code_hash: Option<&str>,
114) -> Result<InkProject> {
115 let mut endpoints = vec![
117 format!("{}/api/contract/{}/metadata", explorer_url, contract_address),
119 format!("{}/api/contracts/{}/abi", explorer_url, contract_address),
120 format!("{}/api/v1/contracts/{}/metadata", explorer_url, contract_address),
121 ];
122
123 if let Some(hash) = code_hash {
125 endpoints.extend(vec![
126 format!("{}/api/contract/{}/metadata", explorer_url, hash),
127 format!("{}/api/contracts/metadata/{}", explorer_url, hash),
128 format!("{}/api/v1/code/{}/abi", explorer_url, hash),
129 ]);
130 }
131
132 let client = reqwest::Client::builder()
133 .timeout(std::time::Duration::from_secs(30))
134 .build()?;
135
136 for url in endpoints {
137 match client.get(&url).send().await {
138 Ok(response) if response.status().is_success() => {
139 if let Ok(metadata) = response.json::<InkProject>().await {
141 return Ok(metadata);
142 }
143 }
144 Ok(response) => {
145 eprintln!(
147 " Endpoint {} returned status: {}",
148 url,
149 response.status()
150 );
151 }
152 Err(e) => {
153 eprintln!(" Failed to connect to {}: {}", url, e);
155 }
156 }
157 }
158
159 Err(anyhow::anyhow!(
160 "No explorer endpoint returned valid metadata for contract {}",
161 contract_address
162 ))
163}
164
165fn try_load_from_cache(cache_dir: &str, contract_address: &str) -> Result<InkProject> {
167 let cache_path = Path::new(cache_dir).join(format!("{}.json", contract_address));
168
169 if !cache_path.exists() {
170 anyhow::bail!("No cache found");
171 }
172
173 load_metadata_from_file(cache_path.to_str().unwrap())
174}
175
176fn save_to_cache(cache_dir: &str, contract_address: &str, metadata: &InkProject) -> Result<()> {
178 std::fs::create_dir_all(cache_dir)?;
179
180 let cache_path = Path::new(cache_dir).join(format!("{}.json", contract_address));
181
182 let json = serde_json::to_string_pretty(metadata)?;
183 std::fs::write(cache_path, json)?;
184
185 Ok(())
186}
187
188pub fn get_default_cache_dir() -> Result<String> {
190 let home = dirs::home_dir().ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
191
192 let cache_dir = home.join(".glin-forge").join("cache");
193
194 Ok(cache_dir.to_string_lossy().to_string())
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_load_metadata_from_json() {
203 }
206
207 #[test]
208 fn test_get_default_cache_dir() {
209 let cache_dir = get_default_cache_dir();
210 assert!(cache_dir.is_ok());
211 assert!(cache_dir.unwrap().contains(".glin-forge"));
212 }
213}