From 2633166705b8d83c3f0587e90308e057ec808e0c Mon Sep 17 00:00:00 2001 From: Dagur Valberg Johannsson Date: Wed, 29 Apr 2026 09:26:32 +0200 Subject: [PATCH] bcmr: Look for well-known on bare domains According to BCMR spec, if bare domains are un op_return, we should look up "well known" path for the domain. Fallback to previous behaviour if well known is not found. --- src/bcmr/bcmrdownloader.rs | 146 +++++++++++++++++++++++++++++++------ 1 file changed, 122 insertions(+), 24 deletions(-) diff --git a/src/bcmr/bcmrdownloader.rs b/src/bcmr/bcmrdownloader.rs index 6374a13..82619c6 100644 --- a/src/bcmr/bcmrdownloader.rs +++ b/src/bcmr/bcmrdownloader.rs @@ -36,6 +36,45 @@ const MAX_BCMR_SIZE: usize = 1024 * 1024 * 100; // 100 MB const MAX_PARALLEL_DOWNLOADS: usize = 5; +const WELL_KNOWN_PATH: &str = "/.well-known/bitcoin-cash-metadata-registry.json"; + +/// Resolve an on-chain BCMR URI into one or more candidate URLs to try in order. +/// +/// Handles three cases: +/// - `ipfs://` → a single random IPFS gateway URL. +/// - Bare domain (authority only, no path) → BCMR v2 well-known URI first, then +/// the bare root URL as a fallback for bug-compatibility with tokens that +/// served the registry directly at `/`. +/// - Anything with a path → used as-is (with `https://` prepended if missing). +fn resolve_uri_candidates(uri: &str) -> Vec { + if let Some(stripped) = uri.strip_prefix("ipfs://") { + return vec![format!( + "{}{}", + IPFS_GATEWAYS.choose(&mut thread_rng()).unwrap(), + stripped + )]; + } + + let (scheme, rest) = match uri.split_once("://") { + Some((s, r)) => (s, r), + None => ("https", uri), + }; + + let (authority, path) = match rest.split_once('/') { + Some((a, p)) => (a, format!("/{p}")), + None => (rest, String::new()), + }; + + if path.is_empty() || path == "/" { + vec![ + format!("{scheme}://{authority}{WELL_KNOWN_PATH}"), + format!("{scheme}://{authority}/"), + ] + } else { + vec![format!("{scheme}://{authority}{path}")] + } +} + pub struct BCMRDownloader { db: SqlitePool, keep_running: Arc, @@ -67,31 +106,21 @@ async fn fetch_bcmr( let mut errors: Vec = Vec::new(); for url in urls { - let url = if let Some(stripped) = url.strip_prefix("ipfs://") { - format!( - "{}{}", - IPFS_GATEWAYS.choose(&mut thread_rng()).unwrap(), - stripped - ) - } else if !url.starts_with("https://") { - format!("https://{url}") - } else { - url.to_string() - }; + for resolved in resolve_uri_candidates(url) { + let (contents, actual_hash) = + match get_url(client, &resolved, DOWNLOAD_TIMEOUT, MAX_BCMR_SIZE).await { + Ok(c) => c, + Err(e) => { + errors.push(e.to_string()); + continue; + } + }; - let (contents, actual_hash) = - match get_url(client, &url, DOWNLOAD_TIMEOUT, MAX_BCMR_SIZE).await { - Ok(c) => c, - Err(e) => { - errors.push(e.to_string()); - continue; - } - }; - - if actual_hash == expected_hash { - return (Some((contents, actual_hash)), String::default(), false); - } else { - candidate = Some((contents, actual_hash)) + if actual_hash == expected_hash { + return (Some((contents, actual_hash)), String::default(), false); + } else { + candidate = Some((contents, actual_hash)) + } } } @@ -272,3 +301,72 @@ impl Drop for BCMRDownloader { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn bare_domain_resolves_to_well_known_then_root() { + let candidates = resolve_uri_candidates("chipnet-16.paryonusd.com"); + assert_eq!( + candidates, + vec![ + "https://chipnet-16.paryonusd.com/.well-known/bitcoin-cash-metadata-registry.json" + .to_string(), + "https://chipnet-16.paryonusd.com/".to_string(), + ] + ); + } + + #[test] + fn bare_domain_with_scheme_and_trailing_slash() { + let candidates = resolve_uri_candidates("https://example.com/"); + assert_eq!( + candidates, + vec![ + "https://example.com/.well-known/bitcoin-cash-metadata-registry.json".to_string(), + "https://example.com/".to_string(), + ] + ); + } + + #[test] + fn url_with_path_is_used_as_is() { + let candidates = resolve_uri_candidates("https://example.com/path/registry.json"); + assert_eq!( + candidates, + vec!["https://example.com/path/registry.json".to_string()] + ); + } + + #[test] + fn schemeless_url_with_path_gets_https() { + let candidates = resolve_uri_candidates("example.com/registry.json"); + assert_eq!( + candidates, + vec!["https://example.com/registry.json".to_string()] + ); + } + + #[test] + fn ipfs_uri_uses_a_gateway() { + let candidates = resolve_uri_candidates("ipfs://QmAbc123"); + assert_eq!(candidates.len(), 1); + assert!(candidates[0].ends_with("/QmAbc123")); + assert!(IPFS_GATEWAYS.iter().any(|gw| candidates[0].starts_with(gw))); + } + + #[test] + fn domain_with_port_is_treated_as_bare() { + let candidates = resolve_uri_candidates("example.com:8443"); + assert_eq!( + candidates, + vec![ + "https://example.com:8443/.well-known/bitcoin-cash-metadata-registry.json" + .to_string(), + "https://example.com:8443/".to_string(), + ] + ); + } +}