glin_contracts/
metadata_fetcher.rs

1// Multi-strategy metadata fetching for ink! contracts
2
3use anyhow::{Context, Result};
4use ink_metadata::InkProject;
5use std::path::Path;
6
7use glin_client::GlinClient;
8
9/// Options for fetching metadata
10pub struct MetadataFetchOptions {
11    pub local_path: Option<String>,
12    pub explorer_url: Option<String>,
13    pub cache_dir: Option<String>,
14}
15
16/// Fetch contract metadata using cascading fallback strategy
17///
18/// Strategy priority:
19/// 1. Local file (if provided via --metadata flag)
20/// 2. Local cache (~/.glin-forge/cache/{address}.json)
21/// 3. Get code_hash from chain ContractInfoOf storage
22/// 4. Fetch from explorer API using code_hash
23/// 5. Fail with helpful error message
24pub async fn fetch_contract_metadata(
25    client: &GlinClient,
26    contract_address: &str,
27    options: MetadataFetchOptions,
28) -> Result<InkProject> {
29    // Strategy 1: Load from local file if provided
30    if let Some(path) = options.local_path {
31        return load_metadata_from_file(&path);
32    }
33
34    // Strategy 2: Try loading from cache
35    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    // Strategy 3: Get code hash from blockchain
42    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    // Strategy 4: Fetch from explorer API
48    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                // Cache it for future use
52                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                // Continue to error
60            }
61        }
62    }
63
64    // All strategies failed
65    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
81/// Load metadata from local file (.json or .contract bundle)
82fn 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    // Check if it's a .contract bundle file
87    if path.ends_with(".contract") {
88        let bundle: serde_json::Value =
89            serde_json::from_str(&content).context("Invalid .contract bundle format")?;
90
91        // Extract spec from bundle
92        let metadata: InkProject = serde_json::from_value(bundle["spec"].clone())
93            .context("Invalid metadata in .contract bundle")?;
94
95        Ok(metadata)
96    } else {
97        // Pure metadata JSON
98        let metadata: InkProject =
99            serde_json::from_str(&content).context("Invalid metadata JSON format")?;
100
101        Ok(metadata)
102    }
103}
104
105/// Fetch metadata from explorer API
106async fn fetch_from_explorer(
107    explorer_url: &str,
108    contract_address: &str,
109    code_hash: Option<&str>,
110) -> Result<InkProject> {
111    // Try common explorer API endpoints with both contract address and code hash
112    let mut endpoints = vec![
113        // Try contract address first
114        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 we have code hash, also try those endpoints
126    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                // Try to parse as InkProject directly
142                if let Ok(metadata) = response.json::<InkProject>().await {
143                    return Ok(metadata);
144                }
145            }
146            Ok(response) => {
147                // Log non-success status
148                eprintln!(
149                    "    Endpoint {} returned status: {}",
150                    url,
151                    response.status()
152                );
153            }
154            Err(e) => {
155                // Log connection errors
156                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
167/// Try to load metadata from local cache
168fn 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
178/// Save metadata to local cache
179fn 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
190/// Get default cache directory
191pub 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        // This test would require a sample metadata file
206        // Skipping for now
207    }
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}