diff --git a/crates/torii/graphql/src/object/erc/erc_token.rs b/crates/torii/graphql/src/object/erc/erc_token.rs index 810246a046..af18bfc0ff 100644 --- a/crates/torii/graphql/src/object/erc/erc_token.rs +++ b/crates/torii/graphql/src/object/erc/erc_token.rs @@ -422,10 +422,8 @@ impl ResolvableObject for TokenObject { v.to_string().trim_matches('"').to_string() }); - let contract_address: String = - row.get("contract_address"); - let image_path = format!("{}/image", contract_address); - + let image_path = + format!("{}/image", id.replace(":", "/")); ( metadata_str, metadata_name, @@ -501,9 +499,7 @@ fn create_token_metadata_from_row(row: &SqliteRow) -> sqlx::Result let metadata_attributes = metadata.get("attributes").map(|v| v.to_string().trim_matches('"').to_string()); - let contract_address: String = row.get("contract_address"); - let image_path = format!("{}/image", contract_address); - + let image_path = format!("{}/image", id.replace(":", "/")); (metadata_str, metadata_name, metadata_description, metadata_attributes, image_path) }; diff --git a/crates/torii/server/src/artifacts.rs b/crates/torii/server/src/artifacts.rs index 0bdec6cc93..dffda63f0b 100644 --- a/crates/torii/server/src/artifacts.rs +++ b/crates/torii/server/src/artifacts.rs @@ -8,7 +8,6 @@ use camino::Utf8PathBuf; use data_url::mime::Mime; use data_url::DataUrl; use image::{DynamicImage, ImageFormat}; -use reqwest::Client; use serde::{Deserialize, Serialize}; use sqlx::{Pool, Sqlite}; use tokio::fs; @@ -16,7 +15,7 @@ use tokio::fs::File; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::sync::broadcast::Receiver; use torii_sqlite::constants::TOKENS_TABLE; -use torii_sqlite::utils::fetch_content_from_ipfs; +use torii_sqlite::utils::{fetch_content_from_http, fetch_content_from_ipfs}; use tracing::{debug, error, trace}; use warp::http::Response; use warp::path::Tail; @@ -59,10 +58,7 @@ async fn serve_static_file( let token_id = format!("{}:{}", parts[0], parts[1]); if !token_image_dir.exists() { - match fetch_and_process_image(&artifacts_dir, &token_id, pool) - .await - .context(format!("Failed to fetch and process image for token_id: {}", token_id)) - { + match fetch_and_process_image(&artifacts_dir, &token_id, pool).await { Ok(path) => path, Err(e) => { error!(error = %e, "Failed to fetch and process image for token_id: {}", token_id); @@ -177,15 +173,8 @@ async fn fetch_and_process_image( uri if uri.starts_with("http") || uri.starts_with("https") => { debug!(image_uri = %uri, "Fetching image from http/https URL"); // Fetch image from HTTP/HTTPS URL - let client = Client::new(); - let response = client - .get(uri) - .send() - .await - .context("Failed to fetch image from URL")? - .bytes() - .await - .context("Failed to read image bytes from response")?; + let response = + fetch_content_from_http(&uri).await.context("Failed to fetch image from URL")?; // svg files typically start with String { result } +// Global clients +static HTTP_CLIENT: Lazy = Lazy::new(|| { + Client::builder() + .timeout(Duration::from_secs(10)) + .pool_idle_timeout(Duration::from_secs(90)) + .build() + .expect("Failed to create HTTP client") +}); + +static IPFS_CLIENT: Lazy = Lazy::new(|| { + IpfsClient::from_str(IPFS_CLIENT_URL) + .expect("Failed to create IPFS client") + .with_credentials(IPFS_CLIENT_USERNAME, IPFS_CLIENT_PASSWORD) +}); + +const INITIAL_BACKOFF: Duration = Duration::from_millis(100); + +/// Fetch content from HTTP URL with retries +pub async fn fetch_content_from_http(url: &str) -> Result { + let mut retries = 0; + let mut backoff = INITIAL_BACKOFF; + + loop { + match HTTP_CLIENT.get(url).send().await { + Ok(response) => { + if !response.status().is_success() { + return Err(anyhow::anyhow!( + "HTTP request failed with status: {}", + response.status() + )); + } + return response.bytes().await.map_err(Into::into); + } + Err(e) => { + if retries >= REQ_MAX_RETRIES { + return Err(anyhow::anyhow!("HTTP request failed: {}", e)); + } + debug!(error = %e, retry = retries, "Request failed, retrying after backoff"); + tokio::time::sleep(backoff).await; + retries += 1; + backoff *= 2; + } + } + } +} + +/// Fetch content from IPFS with retries pub async fn fetch_content_from_ipfs(cid: &str) -> Result { - let mut retries = IPFS_CLIENT_MAX_RETRY; - let client = IpfsClient::from_str(IPFS_CLIENT_URL)? - .with_credentials(IPFS_CLIENT_USERNAME, IPFS_CLIENT_PASSWORD); + let mut retries = 0; + let mut backoff = INITIAL_BACKOFF; - while retries > 0 { - let response = client.cat(cid).map_ok(|chunk| chunk.to_vec()).try_concat().await; - match response { + loop { + match IPFS_CLIENT.cat(cid).map_ok(|chunk| chunk.to_vec()).try_concat().await { Ok(stream) => return Ok(Bytes::from(stream)), Err(e) => { - retries -= 1; - debug!( - error = %e, - remaining_attempts = retries, - cid = cid, - "Failed to fetch content from IPFS, retrying after delay" - ); - tokio::time::sleep(Duration::from_secs(3)).await; + if retries >= REQ_MAX_RETRIES { + return Err(anyhow::anyhow!("IPFS request failed: {}", e)); + } + debug!(error = %e, retry = retries, "Request failed, retrying after backoff"); + tokio::time::sleep(backoff).await; + retries += 1; + backoff *= 2; } } } - - Err(anyhow::anyhow!(format!( - "Failed to pull data from IPFS after {} attempts, cid: {}", - IPFS_CLIENT_MAX_RETRY, cid - ))) } // type used to do calculation on inmemory balances