diff --git a/bin/sozo/src/commands/migrate.rs b/bin/sozo/src/commands/migrate.rs index 713eb8bc73..0e63fe53e9 100644 --- a/bin/sozo/src/commands/migrate.rs +++ b/bin/sozo/src/commands/migrate.rs @@ -3,6 +3,7 @@ use clap::Args; use colored::Colorize; use dojo_utils::{self, TxnConfig}; use dojo_world::contracts::WorldContract; +use dojo_world::metadata::IpfsMetadataService; use scarb::core::{Config, Workspace}; use sozo_ops::migrate::{Migration, MigrationResult}; use sozo_ops::migration_ui::MigrationUi; @@ -19,6 +20,11 @@ use super::options::transaction::TransactionOptions; use super::options::world::WorldOptions; use crate::utils; +// TODO: to remove and to be read from environment variables +const IPFS_CLIENT_URL: &str = "https://ipfs.infura.io:5001"; +const IPFS_USERNAME: &str = "2EBrzr7ZASQZKH32sl2xWauXPSA"; +const IPFS_PASSWORD: &str = "12290b883db9138a8ae3363b6739d220"; + #[derive(Debug, Clone, Args)] pub struct MigrateArgs { #[command(flatten)] @@ -75,7 +81,18 @@ impl MigrateArgs { let MigrationResult { manifest, has_changes } = migration.migrate(&mut spinner).await.context("Migration failed.")?; - migration.upload_metadata(&mut spinner).await.context("Metadata upload failed.")?; + match IpfsMetadataService::new(IPFS_CLIENT_URL, IPFS_USERNAME, IPFS_PASSWORD) { + Ok(mut metadata_service) => { + migration + .upload_metadata(&mut spinner, &mut metadata_service) + .await + .context("Metadata upload failed.")?; + } + _ => { + // Unable to instanciate IPFS service so metadata upload is ignored. + // TODO: add a message. + } + }; spinner.update_text("Writing manifest..."); ws.write_manifest_profile(manifest).context("🪦 Failed to write manifest.")?; diff --git a/crates/dojo/world/src/metadata/fake_metadata_service.rs b/crates/dojo/world/src/metadata/fake_metadata_service.rs new file mode 100644 index 0000000000..083e1bd670 --- /dev/null +++ b/crates/dojo/world/src/metadata/fake_metadata_service.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; +use std::hash::{DefaultHasher, Hash, Hasher}; + +use anyhow::Result; + +use super::metadata_service::MetadataService; + +/// Fake implementation of MetadataService to be used for tests only. +/// It just stores uri and data in a HashMap when `upload` is called, +/// and returns these data when `get` is called. + +#[derive(Debug, Default)] +pub struct FakeMetadataService { + data: HashMap>, +} + +#[allow(async_fn_in_trait)] +impl MetadataService for FakeMetadataService { + async fn upload(&mut self, data: Vec) -> Result { + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + let hash = hasher.finish(); + + let uri = format!("ipfs://{:x}", hash); + self.data.insert(uri.clone(), data); + + Ok(uri) + } + + #[cfg(test)] + async fn get(&self, uri: String) -> Result> { + Ok(self.data.get(&uri).cloned().unwrap_or(Vec::::new())) + } +} diff --git a/crates/dojo/world/src/metadata/ipfs.rs b/crates/dojo/world/src/metadata/ipfs.rs deleted file mode 100644 index cf732c67b3..0000000000 --- a/crates/dojo/world/src/metadata/ipfs.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::io::Cursor; - -use anyhow::Result; -#[cfg(test)] -use futures::TryStreamExt; -use ipfs_api_backend_hyper::{IpfsApi, TryFromUri}; - -const IPFS_CLIENT_URL: &str = "https://ipfs.infura.io:5001"; -const IPFS_USERNAME: &str = "2EBrzr7ZASQZKH32sl2xWauXPSA"; -const IPFS_PASSWORD: &str = "12290b883db9138a8ae3363b6739d220"; - -pub struct IpfsClient { - client: ipfs_api_backend_hyper::IpfsClient, -} - -impl IpfsClient { - pub fn new() -> Result { - Ok(Self { - client: ipfs_api_backend_hyper::IpfsClient::from_str(IPFS_CLIENT_URL)? - .with_credentials(IPFS_USERNAME, IPFS_PASSWORD), - }) - } - - /// Upload a `data` on IPFS and get a IPFS URI. - /// - /// # Arguments - /// * `data`: the data to upload - /// - /// # Returns - /// Result - returns the IPFS URI or a Anyhow error. - pub(crate) async fn upload(&self, data: T) -> Result - where - T: AsRef<[u8]> + std::marker::Send + std::marker::Sync + std::marker::Unpin + 'static, - { - let reader = Cursor::new(data); - let response = self.client.add(reader).await?; - Ok(format!("ipfs://{}", response.hash)) - } - - #[cfg(test)] - pub(crate) async fn get(&self, uri: String) -> Result> { - let res = self - .client - .cat(&uri.replace("ipfs://", "")) - .map_ok(|chunk| chunk.to_vec()) - .try_concat() - .await?; - Ok(res) - } -} diff --git a/crates/dojo/world/src/metadata/ipfs_service.rs b/crates/dojo/world/src/metadata/ipfs_service.rs new file mode 100644 index 0000000000..ea1d11d30e --- /dev/null +++ b/crates/dojo/world/src/metadata/ipfs_service.rs @@ -0,0 +1,71 @@ +use std::io::Cursor; + +use anyhow::Result; +#[cfg(test)] +use futures::TryStreamExt; +use ipfs_api_backend_hyper::{IpfsApi, TryFromUri}; + +use super::metadata_service::MetadataService; + +/// IPFS implementation of MetadataService, allowing to +/// upload metadata to IPFS. +pub struct IpfsMetadataService { + client: ipfs_api_backend_hyper::IpfsClient, +} + +// impl required by clippy +impl std::fmt::Debug for IpfsMetadataService { + fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + Ok(()) + } +} + +impl IpfsMetadataService { + /// Instanciate a new IPFS Metadata service with IPFS credentials. + /// + /// # Arguments + /// * `client_url` - The IPFS client URL + /// * `username` - The IPFS username + /// * `password` - The IPFS password + /// + /// # Returns + /// A new `IpfsMetadataService` is the IPFS client has been successfully + /// instanciated or a Anyhow error if not. + pub fn new(client_url: &str, username: &str, password: &str) -> Result { + if client_url.is_empty() || username.is_empty() || password.is_empty() { + anyhow::bail!("Invalid credentials: empty values not allowed"); + } + if !client_url.starts_with("http://") && !client_url.starts_with("https://") { + anyhow::bail!("Invalid client URL: must start with http:// or https://"); + } + + Ok(Self { + client: ipfs_api_backend_hyper::IpfsClient::from_str(client_url)? + .with_credentials(username, password), + }) + } +} + +#[allow(async_fn_in_trait)] +impl MetadataService for IpfsMetadataService { + async fn upload(&mut self, data: Vec) -> Result { + let reader = Cursor::new(data); + let response = self + .client + .add(reader) + .await + .map_err(|e| anyhow::anyhow!("Failed to upload to IPFS: {}", e))?; + Ok(format!("ipfs://{}", response.hash)) + } + + #[cfg(test)] + async fn get(&self, uri: String) -> Result> { + let res = self + .client + .cat(&uri.replace("ipfs://", "")) + .map_ok(|chunk| chunk.to_vec()) + .try_concat() + .await?; + Ok(res) + } +} diff --git a/crates/dojo/world/src/metadata/metadata_service.rs b/crates/dojo/world/src/metadata/metadata_service.rs new file mode 100644 index 0000000000..7d215b2865 --- /dev/null +++ b/crates/dojo/world/src/metadata/metadata_service.rs @@ -0,0 +1,26 @@ +use anyhow::Result; + +/// MetadataService trait to be implemented to upload +/// some metadata on a specific storage system. +#[allow(async_fn_in_trait)] +pub trait MetadataService: std::marker::Send + std::marker::Sync + std::marker::Unpin { + /// Upload some bytes (`data`) to the storage system, + /// and get back a string URI. + /// + /// # Arguments + /// * `data` - bytes to upload + /// + /// # Returns + /// A string URI or a Anyhow error. + async fn upload(&mut self, data: Vec) -> Result; + + /// Read stored bytes from a URI. (for tests only) + /// + /// # Arguments + /// * `uri` - the URI of the data to read + /// + /// # Returns + /// the read bytes or a Anyhow error. + #[cfg(test)] + async fn get(&self, uri: String) -> Result>; +} diff --git a/crates/dojo/world/src/metadata/metadata_storage.rs b/crates/dojo/world/src/metadata/metadata_storage.rs new file mode 100644 index 0000000000..07d289a182 --- /dev/null +++ b/crates/dojo/world/src/metadata/metadata_storage.rs @@ -0,0 +1,116 @@ +use std::hash::{DefaultHasher, Hash, Hasher}; + +use anyhow::{Context, Result}; +use serde_json::json; +use starknet_crypto::Felt; + +use super::metadata_service::MetadataService; +use crate::config::metadata_config::{ResourceMetadata, WorldMetadata}; +use crate::uri::Uri; + +/// Helper function to compute metadata hash. +/// +/// # Arguments +/// * `data` - the data to hash. +/// +/// # Returns +/// The hash value. +fn compute_metadata_hash(data: T) -> u64 +where + T: Hash, +{ + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + hasher.finish() +} + +/// Helper function to process an optional URI. +/// +/// If the URI is set and refer to a local asset, this asset +/// is then uploaded using the provided MetadataService. +/// In any other case, the URI is kept as it is. +/// +/// # Arguments +/// * `uri` - The URI to process +/// * `service` - The metadata service to use to upload assets. +/// +/// # Returns +/// The updated URI or a Anyhow error. +async fn upload_uri(uri: &Option, service: &mut impl MetadataService) -> Result> { + if let Some(Uri::File(path)) = uri { + let data = std::fs::read(path)?; + let uploaded_uri = Uri::Ipfs(service.upload(data).await?); + Ok(Some(uploaded_uri)) + } else { + Ok(uri.clone()) + } +} + +/// Trait to be implemented by metadata structs to be +/// uploadable on a storage system. +#[allow(async_fn_in_trait)] +pub trait MetadataStorage { + /// Upload metadata using the provided service. + /// + /// # Arguments + /// * `service` - service to use to upload metadata + /// + /// # Returns + /// The uploaded metadata URI or a Anyhow error. + async fn upload(&self, service: &mut impl MetadataService) -> Result; + + /// Upload metadata using the provided service, only if it has changed. + /// + /// # Arguments + /// * `service` - service to use to upload metadata + /// * `current_hash` - the hash of the previously uploaded metadata + /// + /// # Returns + /// The uploaded metadata URI or a Anyhow error. + async fn upload_if_changed( + &self, + service: &mut impl MetadataService, + current_hash: Felt, + ) -> Result> + where + Self: std::hash::Hash, + { + let new_hash = compute_metadata_hash(self); + let new_hash = Felt::from_raw([0, 0, 0, new_hash]); + + if new_hash != current_hash { + let new_uri = self.upload(service).await?; + return Ok(Some((new_uri, new_hash))); + } + + Ok(None) + } +} + +#[allow(async_fn_in_trait)] +impl MetadataStorage for WorldMetadata { + async fn upload(&self, service: &mut impl MetadataService) -> Result { + let mut meta = self.clone(); + + meta.icon_uri = + upload_uri(&self.icon_uri, service).await.context("Failed to upload icon URI")?; + meta.cover_uri = + upload_uri(&self.cover_uri, service).await.context("Failed to upload cover URI")?; + + let serialized = json!(meta).to_string(); + service.upload(serialized.as_bytes().to_vec()).await.context("Failed to upload metadata") + } +} + +#[allow(async_fn_in_trait)] +impl MetadataStorage for ResourceMetadata { + async fn upload(&self, service: &mut impl MetadataService) -> Result { + let mut meta = self.clone(); + + meta.icon_uri = + upload_uri(&self.icon_uri, service).await.context("Failed to upload icon URI")?; + + let serialized = json!(meta).to_string(); + service.upload(serialized.as_bytes().to_vec()).await.context("Failed to upload metadata") + } +} diff --git a/crates/dojo/world/src/metadata/metadata_test.rs b/crates/dojo/world/src/metadata/metadata_test.rs index 6fac76f683..efbe855613 100644 --- a/crates/dojo/world/src/metadata/metadata_test.rs +++ b/crates/dojo/world/src/metadata/metadata_test.rs @@ -7,27 +7,29 @@ use std::str::FromStr; use starknet_crypto::Felt; use url::Url; -use crate::metadata::ipfs::IpfsClient; -use crate::metadata::{MetadataStorage, ResourceMetadata, WorldMetadata}; +use super::fake_metadata_service::FakeMetadataService; +use super::metadata_service::MetadataService; +use super::metadata_storage::MetadataStorage; +use crate::config::metadata_config::{ResourceMetadata, WorldMetadata}; use crate::uri::Uri; +/// Helper function to create a local file absolute path +/// from a relative path. +fn test_file_path(filename: &str) -> PathBuf { + fs::canonicalize( + PathBuf::from_str(&format!("./src/metadata/metadata_test_data/{}", filename)).unwrap(), + ) + .unwrap() +} + +/// Helper function to build a WorldMetadata for tests. fn build_world_metadata() -> WorldMetadata { WorldMetadata { name: "world".to_string(), seed: "world seed".to_string(), description: Some("world description".to_string()), - cover_uri: Some(Uri::File( - fs::canonicalize( - PathBuf::from_str("./src/metadata/metadata_test_data/cover.png").unwrap(), - ) - .unwrap(), - )), - icon_uri: Some(Uri::File( - fs::canonicalize( - PathBuf::from_str("./src/metadata/metadata_test_data/icon.png").unwrap(), - ) - .unwrap(), - )), + cover_uri: Some(Uri::File(test_file_path("cover.png"))), + icon_uri: Some(Uri::File(test_file_path("icon.png"))), website: Some(Url::parse("https://my_world.com").expect("parsing failed")), socials: Some(HashMap::from([ ("twitter".to_string(), "twitter_url".to_string()), @@ -36,39 +38,37 @@ fn build_world_metadata() -> WorldMetadata { } } +/// Helper function to build a ResourceMetadata for tests. fn build_resource_metadata() -> ResourceMetadata { ResourceMetadata { name: "my model".to_string(), description: Some("my model description".to_string()), - icon_uri: Some(Uri::File( - fs::canonicalize( - PathBuf::from_str("./src/metadata/metadata_test_data/icon.png").unwrap(), - ) - .unwrap(), - )), + icon_uri: Some(Uri::File(test_file_path("icon.png"))), } } +// Helper function to check IPFS URI. fn assert_ipfs_uri(uri: &Option) { if let Some(uri) = uri { assert!(uri.to_string().starts_with("ipfs://")); } } -async fn assert_ipfs_content(uri: String, path: PathBuf) { - let ipfs_client = IpfsClient::new().expect("Ipfs client failed"); - let ipfs_data = ipfs_client.get(uri).await.expect("read metadata failed"); +// Helper function to check IPFS content. +async fn assert_ipfs_content(service: &FakeMetadataService, uri: String, path: PathBuf) { + let ipfs_data = service.get(uri).await.expect("read metadata failed"); let expected_data = std::fs::read(path).expect("read local data failed"); - assert_eq!(ipfs_data, expected_data); } #[tokio::test] async fn test_world_metadata() { + let mut metadata_service = FakeMetadataService::default(); + let world_metadata = build_world_metadata(); // first metadata upload without existing hash. - let res = world_metadata.upload_if_changed(Felt::ZERO).await; + let res = world_metadata.upload_if_changed(&mut metadata_service, Felt::ZERO).await; let (current_uri, current_hash) = if let Ok(Some(res)) = res { res @@ -77,13 +77,14 @@ async fn test_world_metadata() { }; // no change => the upload is not done. - let res = world_metadata.upload_if_changed(current_hash).await; + let res = world_metadata.upload_if_changed(&mut metadata_service, current_hash).await; assert!(res.is_ok()); assert!(res.unwrap().is_none()); // different hash => metadata are reuploaded. - let res = world_metadata.upload_if_changed(current_hash + Felt::ONE).await; + let res = + world_metadata.upload_if_changed(&mut metadata_service, current_hash + Felt::ONE).await; let (new_uri, new_hash) = if let Ok(Some(res)) = res { res @@ -94,9 +95,8 @@ async fn test_world_metadata() { assert_eq!(new_uri, current_uri); assert_eq!(new_hash, current_hash); - // read back the metadata stored on IPFS to be sure it is correctly written - let ipfs_client = IpfsClient::new().expect("Ipfs client failed"); - let read_metadata = ipfs_client.get(current_uri).await.expect("read metadata failed"); + // read back the metadata from service to be sure it is correctly written + let read_metadata = metadata_service.get(current_uri).await.expect("read metadata failed"); let read_metadata = str::from_utf8(&read_metadata); assert!(read_metadata.is_ok()); @@ -120,6 +120,7 @@ async fn test_world_metadata() { assert_ipfs_uri(&read_metadata.cover_uri); assert_ipfs_content( + &metadata_service, read_metadata.cover_uri.unwrap().to_string(), fs::canonicalize(PathBuf::from_str("./src/metadata/metadata_test_data/cover.png").unwrap()) .unwrap(), @@ -128,21 +129,22 @@ async fn test_world_metadata() { assert_ipfs_uri(&read_metadata.icon_uri); assert_ipfs_content( + &metadata_service, read_metadata.icon_uri.unwrap().to_string(), fs::canonicalize(PathBuf::from_str("./src/metadata/metadata_test_data/icon.png").unwrap()) .unwrap(), ) .await; - - // TODO: would be nice to fake IpfsClient for tests } #[tokio::test] async fn test_resource_metadata() { + let mut metadata_service = FakeMetadataService::default(); + let resource_metadata = build_resource_metadata(); // first metadata upload without existing hash. - let res = resource_metadata.upload_if_changed(Felt::ZERO).await; + let res = resource_metadata.upload_if_changed(&mut metadata_service, Felt::ZERO).await; assert!(res.is_ok()); let res = res.unwrap(); @@ -150,14 +152,15 @@ async fn test_resource_metadata() { let (current_uri, current_hash) = res.unwrap(); // no change => the upload is not done. - let res = resource_metadata.upload_if_changed(current_hash).await; + let res = resource_metadata.upload_if_changed(&mut metadata_service, current_hash).await; assert!(res.is_ok()); let res = res.unwrap(); assert!(res.is_none()); // different hash => metadata are reuploaded. - let res = resource_metadata.upload_if_changed(current_hash + Felt::ONE).await; + let res = + resource_metadata.upload_if_changed(&mut metadata_service, current_hash + Felt::ONE).await; assert!(res.is_ok()); let res = res.unwrap(); @@ -168,8 +171,7 @@ async fn test_resource_metadata() { assert_eq!(new_hash, current_hash); // read back the metadata stored on IPFS to be sure it is correctly written - let ipfs_client = IpfsClient::new().expect("Ipfs client failed"); - let read_metadata = ipfs_client.get(current_uri).await.expect("read metadata failed"); + let read_metadata = metadata_service.get(current_uri).await.expect("read metadata failed"); let read_metadata = str::from_utf8(&read_metadata); assert!(read_metadata.is_ok()); @@ -184,6 +186,7 @@ async fn test_resource_metadata() { assert_ipfs_uri(&read_metadata.icon_uri); assert_ipfs_content( + &metadata_service, read_metadata.icon_uri.unwrap().to_string(), fs::canonicalize(PathBuf::from_str("./src/metadata/metadata_test_data/icon.png").unwrap()) .unwrap(), diff --git a/crates/dojo/world/src/metadata/mod.rs b/crates/dojo/world/src/metadata/mod.rs index 17fa3d7b7f..ae68b19514 100644 --- a/crates/dojo/world/src/metadata/mod.rs +++ b/crates/dojo/world/src/metadata/mod.rs @@ -1,84 +1,11 @@ -use std::hash::{DefaultHasher, Hash, Hasher}; - -use anyhow::Result; -use async_trait::async_trait; -use ipfs::IpfsClient; -use serde_json::json; -use starknet_crypto::Felt; - -use crate::config::metadata_config::{ResourceMetadata, WorldMetadata}; -use crate::uri::Uri; - -mod ipfs; +pub mod ipfs_service; +pub use ipfs_service::IpfsMetadataService; +pub mod metadata_storage; +pub use metadata_storage::MetadataStorage; +pub mod metadata_service; +pub use metadata_service::MetadataService; +pub mod fake_metadata_service; +pub use fake_metadata_service::FakeMetadataService; #[cfg(test)] mod metadata_test; - -/// Helper function to compute metadata hash using the Hash trait impl. -fn compute_metadata_hash(data: T) -> u64 -where - T: Hash, -{ - let mut hasher = DefaultHasher::new(); - data.hash(&mut hasher); - hasher.finish() -} - -#[async_trait] -pub trait MetadataStorage { - async fn upload(&self) -> Result; - - async fn upload_if_changed(&self, current_hash: Felt) -> Result> - where - Self: std::hash::Hash, - { - let new_hash = compute_metadata_hash(self); - let new_hash = Felt::from_raw([0, 0, 0, new_hash]); - - if new_hash != current_hash { - let new_uri = self.upload().await?; - return Ok(Some((new_uri, new_hash))); - } - - Ok(None) - } -} - -#[async_trait] -impl MetadataStorage for WorldMetadata { - async fn upload(&self) -> Result { - let mut meta = self.clone(); - - let ipfs_client = IpfsClient::new()?; - - if let Some(Uri::File(icon)) = &self.icon_uri { - let icon_data = std::fs::read(icon)?; - meta.icon_uri = Some(Uri::Ipfs(ipfs_client.upload(icon_data).await?)); - }; - - if let Some(Uri::File(cover)) = &self.cover_uri { - let cover_data = std::fs::read(cover)?; - meta.cover_uri = Some(Uri::Ipfs(ipfs_client.upload(cover_data).await?)); - }; - - let serialized = json!(meta).to_string(); - ipfs_client.upload(serialized).await - } -} - -#[async_trait] -impl MetadataStorage for ResourceMetadata { - async fn upload(&self) -> Result { - let mut meta = self.clone(); - - let ipfs_client = IpfsClient::new()?; - - if let Some(Uri::File(icon)) = &self.icon_uri { - let icon_data = std::fs::read(icon)?; - meta.icon_uri = Some(Uri::Ipfs(ipfs_client.upload(icon_data).await?)); - }; - - let serialized = json!(meta).to_string(); - ipfs_client.upload(serialized).await - } -} diff --git a/crates/sozo/ops/src/migrate/mod.rs b/crates/sozo/ops/src/migrate/mod.rs index 93683dbe25..748c3e684b 100644 --- a/crates/sozo/ops/src/migrate/mod.rs +++ b/crates/sozo/ops/src/migrate/mod.rs @@ -29,7 +29,7 @@ use dojo_world::contracts::abigen::world::ResourceMetadata; use dojo_world::contracts::WorldContract; use dojo_world::diff::{Manifest, ResourceDiff, WorldDiff, WorldStatus}; use dojo_world::local::ResourceLocal; -use dojo_world::metadata::MetadataStorage; +use dojo_world::metadata::{MetadataService, MetadataStorage}; use dojo_world::remote::ResourceRemote; use dojo_world::{utils, ResourceType}; use starknet::accounts::{ConnectedAccount, SingleOwnerAccount}; @@ -111,7 +111,11 @@ where /// # Arguments /// /// # Returns - pub async fn upload_metadata(&self, ui: &mut MigrationUi) -> anyhow::Result<()> { + pub async fn upload_metadata( + &self, + ui: &mut MigrationUi, + service: &mut impl MetadataService, + ) -> anyhow::Result<()> { ui.update_text("Uploading metadata..."); let mut invoker = Invoker::new(&self.world.account, self.txn_config); @@ -121,7 +125,7 @@ where self.diff.resources.get(&Felt::ZERO).map_or(Felt::ZERO, |r| r.metadata_hash()); let new_metadata = WorldMetadata::from(self.diff.profile_config.world.clone()); - let res = new_metadata.upload_if_changed(current_hash).await?; + let res = new_metadata.upload_if_changed(service, current_hash).await?; if let Some((new_uri, new_hash)) = res { invoker.add_call(self.world.set_metadata_getcall(&ResourceMetadata { @@ -133,19 +137,19 @@ where // contracts if let Some(configs) = &self.diff.profile_config.contracts { - let calls = self.upload_metadata_from_resource_config(configs).await?; + let calls = self.upload_metadata_from_resource_config(service, configs).await?; invoker.extend_calls(calls); } // models if let Some(configs) = &self.diff.profile_config.models { - let calls = self.upload_metadata_from_resource_config(configs).await?; + let calls = self.upload_metadata_from_resource_config(service, configs).await?; invoker.extend_calls(calls); } // events if let Some(configs) = &self.diff.profile_config.events { - let calls = self.upload_metadata_from_resource_config(configs).await?; + let calls = self.upload_metadata_from_resource_config(service, configs).await?; invoker.extend_calls(calls); } @@ -165,6 +169,7 @@ where async fn upload_metadata_from_resource_config( &self, + service: &mut impl MetadataService, config: &[ResourceConfig], ) -> anyhow::Result> { let mut calls = vec![]; @@ -177,7 +182,7 @@ where let new_metadata = metadata_config::ResourceMetadata::from(item.clone()); - let res = new_metadata.upload_if_changed(current_hash).await?; + let res = new_metadata.upload_if_changed(service, current_hash).await?; if let Some((new_uri, new_hash)) = res { calls.push(self.world.set_metadata_getcall(&ResourceMetadata { diff --git a/crates/sozo/ops/src/tests/migration.rs b/crates/sozo/ops/src/tests/migration.rs index 74618f5c5b..24e24f4d54 100644 --- a/crates/sozo/ops/src/tests/migration.rs +++ b/crates/sozo/ops/src/tests/migration.rs @@ -8,6 +8,7 @@ use dojo_utils::TxnConfig; use dojo_world::config::ResourceConfig; use dojo_world::contracts::WorldContract; use dojo_world::diff::WorldDiff; +use dojo_world::metadata::FakeMetadataService; use katana_runner::RunnerCtx; use scarb::compiler::Profile; use sozo_scarbext::WorkspaceExt; @@ -63,7 +64,8 @@ async fn migrate_spawn_and_move(sequencer: &RunnerCtx, with_metadata: bool) -> M let res = migration.migrate(&mut ui).await.expect("Migration spawn-and-move failed."); if with_metadata { - migration.upload_metadata(&mut ui).await.expect("Upload metadata failed"); + let mut service = FakeMetadataService::default(); + migration.upload_metadata(&mut ui, &mut service).await.expect("Upload metadata failed"); } res