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) => {
44            Some(format!("0x{}", hex::encode(&info.code_hash)))
45        }
46        Err(_e) => {
47            None
48        }
49    };
50
51    // Strategy 4: Fetch from explorer API
52    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                // Cache it for future use
56                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                // Continue to error
64            }
65        }
66    }
67
68    // All strategies failed
69    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
85/// Load metadata from local file (.json or .contract bundle)
86fn 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    // Check if it's a .contract bundle file
91    if path.ends_with(".contract") {
92        let bundle: serde_json::Value = serde_json::from_str(&content)
93            .context("Invalid .contract bundle format")?;
94
95        // Extract spec from bundle
96        let metadata: InkProject = serde_json::from_value(bundle["spec"].clone())
97            .context("Invalid metadata in .contract bundle")?;
98
99        Ok(metadata)
100    } else {
101        // Pure metadata JSON
102        let metadata: InkProject =
103            serde_json::from_str(&content).context("Invalid metadata JSON format")?;
104
105        Ok(metadata)
106    }
107}
108
109/// Fetch metadata from explorer API
110async fn fetch_from_explorer(
111    explorer_url: &str,
112    contract_address: &str,
113    code_hash: Option<&str>,
114) -> Result<InkProject> {
115    // Try common explorer API endpoints with both contract address and code hash
116    let mut endpoints = vec![
117        // Try contract address first
118        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 we have code hash, also try those endpoints
124    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                // Try to parse as InkProject directly
140                if let Ok(metadata) = response.json::<InkProject>().await {
141                    return Ok(metadata);
142                }
143            }
144            Ok(response) => {
145                // Log non-success status
146                eprintln!(
147                    "    Endpoint {} returned status: {}",
148                    url,
149                    response.status()
150                );
151            }
152            Err(e) => {
153                // Log connection errors
154                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
165/// Try to load metadata from local cache
166fn 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
176/// Save metadata to local cache
177fn 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
188/// Get default cache directory
189pub 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        // This test would require a sample metadata file
204        // Skipping for now
205    }
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}