Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sozo): upload world/models/contracts metadata only if changed #2691

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

remybar
Copy link
Contributor

@remybar remybar commented Nov 14, 2024

Description

Closes #2096, #1907.

Tests

  • Yes
  • No, because they aren't needed
  • No, because I need help

Added to documentation?

Checklist

  • I've formatted my code (scripts/prettier.sh, scripts/rust_fmt.sh, scripts/cairo_fmt.sh)
  • I've linted my code (scripts/clippy.sh, scripts/docs.sh)
  • I've commented my code
  • I've requested a review after addressing the comments

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for IPFS configuration with new IpfsOptions and IpfsConfig structures.
    • Introduced IpfsService for uploading metadata to IPFS.
    • Enhanced ProfileConfig to manage multiple resource configurations for models, contracts, and events.
    • Added new fields metadata_hash to various structures for improved metadata handling.
  • Improvements

    • Updated migration processes to optionally include metadata uploads.
    • Enhanced event handling for MetadataUpdate events to reflect new metadata structures.
    • Expanded the WorldMetadata and ResourceMetadata structures to support additional metadata fields.
  • Tests

    • Expanded test coverage for metadata handling and IPFS interactions.
    • Introduced new tests for validating metadata uploads and configurations.
  • Bug Fixes

    • Improved error handling in metadata upload processes.

Copy link
Collaborator

@glihm glihm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a suggestion on from where we could fetch the current value.

It's a small change in the WorldRemote, but may be better integrated in the migration flow, wdyt?

crates/sozo/ops/src/migrate/mod.rs Outdated Show resolved Hide resolved
@glihm glihm changed the title feat: upload world/models/contracts metadata only if changed feat(sozo): upload world/models/contracts metadata only if changed Nov 14, 2024
@remybar remybar force-pushed the feat-metadata branch 3 times, most recently from 1a4b450 to f38113c Compare November 15, 2024 14:57
Copy link

codecov bot commented Nov 15, 2024

Codecov Report

Attention: Patch coverage is 80.64516% with 120 lines in your changes missing coverage. Please review.

Project coverage is 56.64%. Comparing base (f6659db) to head (0e51fb9).
Report is 7 commits behind head on main.

Files with missing lines Patch % Lines
bin/sozo/src/commands/migrate.rs 0.00% 30 Missing ⚠️
crates/dojo/world/src/services/ipfs_service.rs 0.00% 27 Missing ⚠️
crates/dojo/world/src/contracts/abigen/world.rs 20.00% 24 Missing ⚠️
crates/dojo/world/src/config/ipfs_config.rs 9.09% 10 Missing ⚠️
crates/sozo/ops/src/migrate/mod.rs 84.37% 10 Missing ⚠️
crates/dojo/world/src/uri.rs 55.55% 8 Missing ⚠️
bin/sozo/src/commands/auth.rs 33.33% 4 Missing ⚠️
crates/dojo/world/src/diff/resource.rs 66.66% 2 Missing ⚠️
crates/dojo/world/src/remote/resource.rs 86.66% 2 Missing ⚠️
bin/sozo/src/commands/dev.rs 0.00% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2691      +/-   ##
==========================================
+ Coverage   56.34%   56.64%   +0.29%     
==========================================
  Files         411      416       +5     
  Lines       52742    53431     +689     
==========================================
+ Hits        29719    30266     +547     
- Misses      23023    23165     +142     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.


🚨 Try these New Features:

@remybar remybar force-pushed the feat-metadata branch 3 times, most recently from 4b304de to 091964a Compare November 17, 2024 16:28
@remybar remybar marked this pull request as ready for review November 17, 2024 16:28
Copy link

coderabbitai bot commented Nov 17, 2024

Walkthrough

Ohayo! This pull request introduces several enhancements primarily focused on metadata handling and IPFS integration. Key changes include the addition of IPFS-related options to the MigrateArgs struct, new metadata fields in various models, and improved upload functionalities for metadata to IPFS. Additionally, the UploadService trait is defined for managing uploads, and several new tests ensure the correctness of these features. The overall structure of the codebase is enhanced to support these functionalities without disrupting existing workflows.

Changes

File Path Change Summary
bin/sozo/src/commands/migrate.rs Enhanced MigrateArgs with new ipfs field; updated run method to handle IPFS metadata upload.
crates/dojo/core/src/model/metadata.cairo Added metadata_hash field to ResourceMetadata struct.
crates/dojo/core-cairo-test/src/tests/world/metadata.cairo Updated tests to include new metadata_hash field in assertions.
crates/dojo/world/src/config/metadata_config.rs Introduced ResourceMetadata struct; added Hash implementation for WorldMetadata and ResourceMetadata.
crates/dojo/world/src/config/profile_config.rs Added optional fields for models, contracts, and events in ProfileConfig.
crates/dojo/world/src/config/resource_config.rs Introduced new ResourceConfig struct for resource management.
crates/dojo/world/src/remote/events_to_remote.rs Updated match_event method to handle MetadataUpdate events.
crates/dojo/world/src/services/ipfs_service.rs Implemented IpfsService for uploading data to IPFS.
crates/dojo/world/src/services/upload_service.rs Defined UploadService trait for handling uploads.
crates/dojo/world/src/constants.rs Added new constant WORLD for resource identification.
crates/dojo/world/src/config/environment.rs Added ipfs_config field to Environment struct for IPFS settings.
bin/sozo/src/commands/options/ipfs.rs Introduced IpfsOptions struct for command-line argument management.
bin/sozo/src/commands/dev.rs Updated MigrateArgs to include ipfs field with default value.
bin/sozo/src/commands/auth.rs Replaced Felt::ZERO with WORLD for permissions management.
examples/spawn-and-move/dojo_dev.toml Added new models, events, and contracts; updated world_address.
crates/dojo/world/src/metadata/metadata_storage.rs Added MetadataStorage trait for metadata upload handling.
crates/dojo/world/src/metadata/mod.rs Added metadata_storage module and re-exported MetadataStorage.
bin/sozo/src/commands/options/mod.rs Added new module declaration for ipfs.
crates/dojo/utils/src/env.rs Introduced constants for IPFS configuration environment variables.
crates/dojo/world/src/services/mod.rs Added new modules for ipfs_service, mock_upload_service, and upload_service.
crates/dojo/world/src/services/mock_upload_service.rs Implemented MockUploadService for testing upload functionality.
crates/dojo/world/src/services/ipfs_service.rs Implemented IpfsService for uploading to IPFS and validating configurations.
crates/dojo/world/src/diff/compare.rs Updated comparison logic to include metadata_hash.
crates/dojo/world/src/diff/mod.rs Added metadata_hash field to WorldStatusInfo.
crates/dojo/world/src/diff/resource.rs Added metadata_hash method to ResourceDiff enum.
crates/dojo/world/src/metadata/metadata_test.rs Updated tests to validate new metadata handling functionalities.
scripts/rust_fmt.sh Enhanced formatting options for cargo fmt.
bin/sozo/tests/test_data/policies.json Updated target addresses and method names in policies.
crates/dojo/world/src/remote/mod.rs Added metadata_hash field to WorldRemote.
crates/dojo/world/src/remote/resource.rs Enhanced CommonRemoteInfo with metadata_hash.

Assessment against linked issues

Objective Addressed Explanation
Ensure metadata uploads occur after failures (#2096)
Implement easy updates for world metadata (#2096)

Possibly related PRs

Suggested reviewers

  • glihm

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Outside diff range and nitpick comments (28)
crates/dojo/core/src/model/metadata.cairo (2)

12-12: Ohayo! The metadata_hash addition looks solid, sensei!

The new metadata_hash field is a well-thought-out solution that enables efficient change detection for metadata updates. This aligns perfectly with the PR's objective of uploading metadata only when necessary, especially after failed deployments.

This approach provides several benefits:

  • Efficient change detection through hash comparison
  • Prevents unnecessary uploads when metadata hasn't changed
  • Enables retry mechanisms after failed deployments

12-12: Consider adding documentation for the metadata_hash field, sensei!

While the implementation is solid, adding documentation comments would help other developers understand the purpose and usage of this new field.

 pub struct ResourceMetadata {
     #[key]
     pub resource_id: felt252,
     pub metadata_uri: ByteArray,
+    /// Hash of the metadata content, used to detect changes and prevent unnecessary uploads
     pub metadata_hash: felt252
 }
examples/spawn-and-move/dojo_dev.toml (2)

18-21: Consider enhancing the event documentation, sensei!

The event description could be more detailed to specify:

  • What data is emitted when a player moves
  • When exactly this event is triggered
  • Who can listen to this event

22-25: Please elaborate on the contract's actions, sensei!

The description "set of actions for a player" could be more specific about:

  • What actions are available
  • Any constraints or prerequisites for these actions
  • How these actions relate to the models and events
crates/dojo/world/src/metadata/ipfs.rs (1)

17-22: Consider adding retry mechanism for IPFS client initialization

The client initialization looks good, but IPFS services can be occasionally unstable. Consider adding retry logic for better reliability.

Example implementation:

use tokio::time::{sleep, Duration};

pub fn new() -> Result<Self> {
    let max_retries = 3;
    let mut attempt = 0;
    
    loop {
        match ipfs_api_backend_hyper::IpfsClient::from_str(IPFS_CLIENT_URL) {
            Ok(client) => return Ok(Self {
                client: client.with_credentials(IPFS_USERNAME, IPFS_PASSWORD),
            }),
            Err(e) if attempt < max_retries => {
                attempt += 1;
                sleep(Duration::from_secs(1)).await;
                continue;
            }
            Err(e) => return Err(e.into()),
        }
    }
}
crates/dojo/world/src/remote/mod.rs (1)

26-27: Consider adding tests for metadata_hash initialization and updates

Ohayo sensei! While the field addition is solid, we should ensure it's properly tested. Consider adding test cases to verify:

  1. Default initialization of metadata_hash
  2. Metadata hash updates through the WorldRemote interface

Here's a suggested test case:

#[test]
fn test_world_metadata_hash() {
    let world_remote = WorldRemote::default();
    assert_eq!(world_remote.metadata_hash, Felt::ZERO, "Default metadata hash should be zero");
}
crates/dojo/core-cairo-test/src/tests/world/metadata.cairo (1)

Line range hint 1-120: Consider adding edge cases for metadata_hash validation, sensei!

While the current test coverage is good, consider adding test cases for:

  1. Zero hash values
  2. Maximum u32 hash values
  3. Hash collisions between different metadata

Here's a suggested test case:

#[test]
fn test_set_metadata_edge_cases() {
    let world = deploy_world();
    let world = world.dispatcher;

    // Test zero hash
    let metadata_zero = ResourceMetadata {
        resource_id: 0,
        metadata_uri: format!("ipfs:zero"),
        metadata_hash: 0
    };
    world.set_metadata(metadata_zero.clone());
    assert(world.metadata(0) == metadata_zero, 'invalid zero hash');

    // Test max hash
    let metadata_max = ResourceMetadata {
        resource_id: 0,
        metadata_uri: format!("ipfs:max"),
        metadata_hash: 0xffffffff
    };
    world.set_metadata(metadata_max.clone());
    assert(world.metadata(0) == metadata_max, 'invalid max hash');
}
crates/dojo/world/src/uri.rs (2)

21-35: Excellent Hash implementation, sensei!

The Hash implementation is well-structured and correctly handles all Uri variants. This aligns perfectly with the PR's objective of tracking metadata changes.

Consider adding test cases for the Hash implementation to ensure consistent behavior across variants. Here's a suggested test:

#[test]
fn test_uri_hash() {
    use std::collections::hash_map::DefaultHasher;
    use std::hash::{Hash, Hasher};

    fn calculate_hash<T: Hash>(t: &T) -> u64 {
        let mut s = DefaultHasher::new();
        t.hash(&mut s);
        s.finish()
    }

    let uri1 = Uri::Ipfs("ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG".to_string());
    let uri2 = Uri::Ipfs("ipfs://QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG".to_string());
    let uri3 = Uri::Ipfs("ipfs://different".to_string());

    assert_eq!(calculate_hash(&uri1), calculate_hash(&uri2));
    assert_ne!(calculate_hash(&uri1), calculate_hash(&uri3));
}

Line range hint 1-69: Ohayo! Great architectural contribution, sensei!

These implementations provide essential building blocks for the PR's objective of uploading metadata only when changed. The Hash implementation enables efficient change detection, while the Display implementation supports better debugging and logging capabilities.

This lays a solid foundation for metadata change tracking. Consider documenting these new capabilities in the module-level documentation to help other developers understand how to leverage them for metadata comparison.

crates/dojo/world/src/diff/resource.rs (2)

116-123: Ohayo! Add documentation for metadata hash semantics

The implementation looks solid, sensei! However, it would be helpful to document why Felt::ZERO is used for newly created resources and how this relates to the metadata upload process.

Consider adding documentation like this:

    /// Returns the current metadata hash of the resource.
+   ///
+   /// For newly created resources that haven't been deployed yet, returns `Felt::ZERO`
+   /// as no metadata has been uploaded. For existing resources, returns the hash of
+   /// their current metadata from the remote state.
    pub fn metadata_hash(&self) -> Felt {

116-123: Implementation aligns well with existing patterns

The implementation follows the established pattern of other methods in the enum, correctly handles all variants, and integrates smoothly with the remote metadata functionality. This change supports the PR objective of tracking metadata changes.

Consider adding a helper method to check if metadata needs updating by comparing the local metadata hash with this remote hash. This would support the PR's goal of uploading "only if changed".

crates/sozo/ops/src/tests/migration.rs (2)

89-111: Add documentation to the helper function, sensei!

While the function is well-structured, it would benefit from documentation explaining:

  • The purpose of the function
  • Parameter descriptions
  • The meaning of the checker function
  • Return value implications

Here's a suggested documentation block:

+/// Validates resource metadata in a WorldDiff against expected criteria
+/// 
+/// # Arguments
+/// * `diff` - The WorldDiff containing resources to check
+/// * `resources` - Optional vector of ResourceConfig to validate
+/// * `expected_count` - Expected number of resources
+/// * `checker` - Function to validate metadata hash values
+/// 
+/// # Panics
+/// Panics if assertions fail for resource count or metadata hash validation

113-150: Excellent test implementation, sensei!

The test thoroughly verifies metadata state transitions and covers all resource types. A few observations:

  1. Good use of helper functions is_set and is_not_set
  2. Comprehensive verification of world and resource metadata
  3. Clear comments explaining the test flow

One suggestion: Consider adding a test case for error conditions when metadata upload fails.

Here's a suggested additional test case:

#[tokio::test(flavor = "multi_thread")]
#[katana_runner::test(accounts = 10)]
async fn upload_metadata_handles_errors(sequencer: &RunnerCtx) {
    // Test implementation for error conditions
    // This would help verify the error handling in the migration process
}
crates/dojo/world/src/diff/compare.rs (1)

Line range hint 17-27: Ohayo! Consider adding metadata_hash comparison in ContractLocal implementation

The comparison logic only checks class_hash but doesn't account for metadata changes. Based on the PR objectives to handle metadata updates, we should also compare metadata_hash.

Here's a suggested implementation:

 impl ComparableResource for ContractLocal {
     fn compare(self, remote: ResourceRemote) -> ResourceDiff {
         let remote_contract = remote.as_contract_or_panic();
 
-        if self.common.class_hash == remote_contract.common.current_class_hash() {
+        if self.common.class_hash == remote_contract.common.current_class_hash() 
+            && self.metadata_hash == remote_contract.common.metadata_hash {
             ResourceDiff::Synced(ResourceLocal::Contract(self), remote)
         } else {
             ResourceDiff::Updated(ResourceLocal::Contract(self), remote)
         }
     }
 }
crates/dojo/world/src/config/profile_config.rs (2)

21-23: Ohayo! Well-structured metadata fields, sensei!

The new optional fields for models, contracts, and events are well-organized and follow a consistent pattern. This structure aligns perfectly with the PR's objective to improve metadata handling.

Consider documenting these fields with rustdoc comments to help future maintainers understand their purpose and usage patterns.


141-154: Ohayo! Consider adding edge cases to the test suite, sensei.

While the current test coverage is good, consider adding these scenarios:

  1. Empty vectors for models/contracts/events
  2. Missing optional fields (description, icon_uri)
  3. Invalid URI formats
  4. Duplicate tags within the same resource type

This will help ensure robust handling of various input scenarios.

Here's a suggested test case to add:

#[test]
fn test_profile_config_edge_cases() {
    let content = r#"
    [world]
    name = "test"
    seed = "abcd"

    [namespace]
    default = "test"

    [[models]]
    tag = "empty-model"

    [[contracts]]
    tag = "invalid-contract"
    icon_uri = "not-a-valid-uri"
    "#;

    let config = toml::from_str::<ProfileConfig>(content).unwrap();
    
    // Test empty models
    assert!(config.models.is_some());
    let models = config.models.unwrap();
    assert_eq!(models[0].description, None);
    assert_eq!(models[0].icon_uri, None);

    // Test invalid URI
    assert!(config.contracts.is_some());
    let contracts = config.contracts.unwrap();
    assert!(contracts[0].icon_uri.is_none());
}

Also applies to: 212-232

crates/dojo/world/src/diff/mod.rs (2)

82-82: Consider documenting the ZERO initialization significance

The initialization of metadata_hash to Felt::ZERO in from_local implies this is a new world without existing metadata, but this significance isn't documented.

-                metadata_hash: Felt::ZERO,
+                // Initialize to ZERO for new worlds without existing metadata
+                metadata_hash: Felt::ZERO,

Line range hint 32-123: Ohayo sensei! Clean and focused metadata tracking implementation

The approach of using a dedicated metadata_hash field in WorldStatusInfo is elegant and maintainable. It:

  1. Cleanly separates metadata state tracking from other world state
  2. Enables efficient metadata change detection
  3. Maintains backward compatibility with existing world diff logic

Consider documenting this design decision in the module-level documentation to help future maintainers understand the rationale.

crates/dojo/world/src/remote/events_to_remote.rs (2)

254-264: LGTM with a minor enhancement suggestion

Ohayo sensei! The MetadataUpdate event handling looks solid with proper separation between world and resource metadata updates.

Consider adding trace logging for world metadata updates similar to resource updates for consistency.

 if e.resource == Felt::ZERO {
     self.metadata_hash = e.hash;
+    trace!("World metadata updated with hash: {:#066x}", e.hash);
 } else {

532-563: Add test coverage for world metadata updates

Ohayo sensei! While the current test thoroughly covers resource metadata updates, we should also add test coverage for world metadata updates (when resource == Felt::ZERO).

#[tokio::test]
async fn test_world_metadata_updated_event() {
    let mut world_remote = WorldRemote::default();
    
    let event = WorldEvent::MetadataUpdate(world::MetadataUpdate {
        resource: Felt::ZERO,
        uri: ByteArray::from_string("ipfs://world").unwrap(),
        hash: Felt::THREE,
    });

    world_remote.match_event(event).unwrap();
    assert_eq!(world_remote.metadata_hash, Felt::THREE);
}
crates/dojo/core/src/world/world_contract.cairo (1)

114-115: Consider adding documentation for the hash field usage, sensei!

While the implementation is correct, it would be helpful to add documentation explaining:

  • The hash calculation method
  • When the hash changes
  • How to handle hash conflicts

Add documentation above the MetadataUpdate event:

+    /// Emitted when a resource's metadata is updated
+    /// * `resource` - The resource identifier
+    /// * `uri` - The metadata URI
+    /// * `hash` - Hash of the metadata content for change detection
     #[derive(Drop, starknet::Event)]
     pub struct MetadataUpdate {

Also applies to: 360-364

crates/dojo/world/src/metadata/mod.rs (3)

46-65: Refactor to eliminate code duplication between implementations

Ohayo, sensei! The upload methods in both WorldMetadata and ResourceMetadata share similar logic for handling metadata uploads. To adhere to the DRY (Don't Repeat Yourself) principle, consider refactoring the common code into a helper function or a generic implementation. This will enhance maintainability and readability.

Here's how you might refactor the code:

  1. Create a generic helper function:

    async fn upload_metadata<T>(meta: &mut T) -> Result<String>
    where
        T: Clone + Serialize + HasIconUri + HasCoverUri,
    {
        let ipfs_client = IpfsClient::new()?;
    
        if let Some(Uri::File(icon)) = meta.icon_uri() {
            let icon_data = std::fs::read(icon)?;
            meta.set_icon_uri(Some(Uri::Ipfs(ipfs_client.upload(icon_data).await?)));
        }
    
        if let Some(Uri::File(cover)) = meta.cover_uri() {
            let cover_data = std::fs::read(cover)?;
            meta.set_cover_uri(Some(Uri::Ipfs(ipfs_client.upload(cover_data).await?)));
        }
    
        let serialized = json!(meta).to_string();
        ipfs_client.upload(serialized).await
    }
  2. Define traits for accessing and modifying URIs:

    trait HasIconUri {
        fn icon_uri(&self) -> &Option<Uri>;
        fn set_icon_uri(&mut self, uri: Option<Uri>);
    }
    
    trait HasCoverUri {
        fn cover_uri(&self) -> &Option<Uri>;
        fn set_cover_uri(&mut self, uri: Option<Uri>);
    }
  3. Implement the traits for your metadata structs:

    impl HasIconUri for WorldMetadata {
        fn icon_uri(&self) -> &Option<Uri> {
            &self.icon_uri
        }
    
        fn set_icon_uri(&mut self, uri: Option<Uri>) {
            self.icon_uri = uri;
        }
    }
    
    impl HasCoverUri for WorldMetadata {
        fn cover_uri(&self) -> &Option<Uri> {
            &self.cover_uri
        }
    
        fn set_cover_uri(&mut self, uri: Option<Uri>) {
            self.cover_uri = uri;
        }
    }
    
    impl HasIconUri for ResourceMetadata {
        fn icon_uri(&self) -> &Option<Uri> {
            &self.icon_uri
        }
    
        fn set_icon_uri(&mut self, uri: Option<Uri>) {
            self.icon_uri = uri;
        }
    }
    
    // ResourceMetadata may not have a cover_uri; implement accordingly
  4. Update the upload methods:

    #[async_trait]
    impl MetadataStorage for WorldMetadata {
        async fn upload(&self) -> Result<String> {
            let mut meta = self.clone();
            upload_metadata(&mut meta).await
        }
    }
    
    #[async_trait]
    impl MetadataStorage for ResourceMetadata {
        async fn upload(&self) -> Result<String> {
            let mut meta = self.clone();
            upload_metadata(&mut meta).await
        }
    }

    Adjust the traits and helper functions based on the specific fields each metadata struct contains.

Also applies to: 67-81


48-48: Assess the impact of cloning large metadata structures

Ohayo, sensei! Cloning self to create meta can be costly if the metadata structures are large. Consider optimizing to clone only the necessary parts or borrow mutably if possible.

Example modification:

  • Instead of cloning the entire object, create a new instance with updated fields or refactor methods to work with references where appropriate.

Also applies to: 69-69


52-60: Handle potential errors when reading files and uploading to IPFS

Ohayo, sensei! When reading files and uploading data to IPFS, consider adding more robust error handling or user-friendly error messages to assist in debugging if file reading fails or upload issues occur.

Example:

  • Wrap file reading and uploading in a custom function that provides context-specific error messages.

Also applies to: 73-76

crates/dojo/world/src/metadata/metadata_test.rs (2)

21-23: Sensei, consider using crate-relative paths to ensure test data is found

Using relative paths like "./src/metadata/metadata_test_data/cover.png" may cause tests to fail if they're run from a different working directory. Ohayo! To make the tests more robust, you might use env!("CARGO_MANIFEST_DIR") to build paths relative to the crate root.

You can modify the path construction as follows:

-PathBuf::from_str("./src/metadata/metadata_test_data/cover.png").unwrap()
+PathBuf::from(format!(
+    "{}/src/metadata/metadata_test_data/cover.png",
+    env!("CARGO_MANIFEST_DIR")
+))

Also applies to: 45-47


137-137: Consider mocking IpfsClient for tests

Ohayo, sensei! The TODO comment suggests that it would be beneficial to fake or mock the IpfsClient for tests to avoid actual IPFS calls and improve test reliability and speed.

Would you like assistance in implementing a mock IpfsClient for your tests?

crates/sozo/ops/src/migrate/mod.rs (2)

172-189: Avoid unnecessary cloning of item

Ohayo, sensei! To improve efficiency, consider passing a reference to item instead of cloning it when creating new_metadata.

Apply this diff:

-        let new_metadata = metadata_config::ResourceMetadata::from(item.clone());
+        let new_metadata = metadata_config::ResourceMetadata::from(item);

154-155: Simplify error handling by avoiding string conversion

Ohayo, sensei! Instead of converting errors to strings and then wrapping them with anyhow!, you can use anyhow::Error::from to preserve error context.

Apply this diff:

-        invoker.multicall().await.map_err(|e| anyhow!(e.to_string()))?;
+        invoker.multicall().await.map_err(anyhow::Error::from)?;

And similarly later:

-        invoker.invoke_all_sequentially().await.map_err(|e| anyhow!(e.to_string()))?;
+        invoker.invoke_all_sequentially().await.map_err(anyhow::Error::from)?;

Also applies to: 160-161

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between f6659db and 091964a.

⛔ Files ignored due to path filters (4)
  • Cargo.lock is excluded by !**/*.lock
  • crates/dojo/world/src/metadata/metadata_test_data/icon.png is excluded by !**/*.png
  • spawn-and-move-db.tar.gz is excluded by !**/*.gz
  • types-test-db.tar.gz is excluded by !**/*.gz
📒 Files selected for processing (27)
  • bin/sozo/src/commands/migrate.rs (1 hunks)
  • crates/dojo/core-cairo-test/src/tests/world/metadata.cairo (6 hunks)
  • crates/dojo/core/src/model/metadata.cairo (1 hunks)
  • crates/dojo/core/src/world/world_contract.cairo (2 hunks)
  • crates/dojo/world/Cargo.toml (1 hunks)
  • crates/dojo/world/src/config/metadata_config.rs (2 hunks)
  • crates/dojo/world/src/config/mod.rs (1 hunks)
  • crates/dojo/world/src/config/profile_config.rs (4 hunks)
  • crates/dojo/world/src/config/resource_config.rs (1 hunks)
  • crates/dojo/world/src/contracts/abigen/world.rs (8 hunks)
  • crates/dojo/world/src/diff/compare.rs (3 hunks)
  • crates/dojo/world/src/diff/mod.rs (3 hunks)
  • crates/dojo/world/src/diff/resource.rs (1 hunks)
  • crates/dojo/world/src/metadata/ipfs.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test_data/abi.json (0 hunks)
  • crates/dojo/world/src/metadata/metadata_test_data/source.cairo (0 hunks)
  • crates/dojo/world/src/metadata/mod.rs (1 hunks)
  • crates/dojo/world/src/metadata/world.rs (0 hunks)
  • crates/dojo/world/src/remote/events_to_remote.rs (3 hunks)
  • crates/dojo/world/src/remote/mod.rs (1 hunks)
  • crates/dojo/world/src/remote/resource.rs (3 hunks)
  • crates/dojo/world/src/uri.rs (3 hunks)
  • crates/sozo/ops/Cargo.toml (1 hunks)
  • crates/sozo/ops/src/migrate/mod.rs (2 hunks)
  • crates/sozo/ops/src/tests/migration.rs (4 hunks)
  • examples/spawn-and-move/dojo_dev.toml (2 hunks)
💤 Files with no reviewable changes (3)
  • crates/dojo/world/src/metadata/metadata_test_data/abi.json
  • crates/dojo/world/src/metadata/metadata_test_data/source.cairo
  • crates/dojo/world/src/metadata/world.rs
🔇 Additional comments (39)
crates/dojo/world/src/config/resource_config.rs (1)

1-4: Ohayo! Clean and minimal imports, looking good!

The imports are well-organized and include only what's necessary for the functionality.

crates/dojo/world/src/config/mod.rs (2)

7-7: Ohayo! Clean module organization, sensei!

The addition of the resource_config module and its public export follows the established pattern and supports the enhanced metadata handling capabilities.

Also applies to: 14-14


7-7: Let's verify the module integration, sensei!

Since this module is part of the metadata handling improvements, let's verify its integration with related modules.

Also applies to: 14-14

✅ Verification successful

Ohayo sensei! The ResourceConfig integration looks solid!

The verification shows that ResourceConfig is well-integrated across the codebase:

  • Used in profile_config.rs for models, contracts, and events configuration
  • Properly integrated with metadata handling through metadata_config.rs
  • Utilized in migration operations (sozo/ops)
  • Correctly exported in the module hierarchy
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the ResourceConfig usage across the codebase
# Expected: Find usage in profile_config.rs and metadata handling code

# Check for ResourceConfig usage
echo "Checking ResourceConfig usage..."
rg -p "ResourceConfig" --type rust

# Check for relationship with metadata handling
echo "Checking metadata-related code..."
rg -p "metadata" --type rust --glob "crates/dojo/world/src/**"

Length of output: 10333

examples/spawn-and-move/dojo_dev.toml (2)

34-34: Verify world address configuration, sensei!

The world address has been updated. Let's ensure this new address is properly configured for metadata handling.

#!/bin/bash
# Search for references to the world address
rg "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea"

# Check for any world initialization or metadata upload scripts
fd -g "*world*.{sh,rs,cairo}" --exec grep -l "metadata"

6-17: Ohayo! Please provide schema details for the models, sensei!

While the models' descriptions are clear, it would be helpful to include the schema information (fields, types, etc.) for each model to ensure proper integration with the metadata handling system.

Let's check if schema information exists elsewhere:

crates/dojo/world/Cargo.toml (1)

33-33: LGTM! The futures dependency addition is well-aligned.

Ohayo! The addition of futures as a dev-dependency makes perfect sense for testing the new async metadata upload functionality, sensei! It complements the existing async-related dependencies like async-trait and tokio.

crates/sozo/ops/Cargo.toml (2)

16-16: Ohayo! The metadata feature addition looks good, sensei!

The addition of the metadata feature to dojo-world aligns perfectly with the PR objectives for improving metadata handling. This change enables the core functionality needed for the new metadata upload process.

Let's verify the feature usage:

✅ Verification successful

Ohayo! The metadata feature is properly utilized, sensei!

The verification shows that the metadata feature from dojo-world is actively used in the sozo crate for:

  • Uploading world and resource metadata to IPFS
  • Managing metadata hashes and URIs
  • Handling metadata storage and updates

The feature addition is necessary and correctly implemented, with clear usage in crates/sozo/ops/src/migrate/mod.rs for the metadata upload functionality.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the metadata feature is being used in the codebase

# Search for metadata-related functionality usage
rg -l "use.*dojo_world.*metadata" 

# Search for potential metadata feature gates
rg "#\[cfg\(feature.*metadata\)\]"

Length of output: 11950


Script:

#!/bin/bash
# Let's verify the metadata feature usage in dojo-world and sozo
# First, check for metadata-related imports and usage in sozo
rg -A 5 "use dojo_world::metadata" crates/sozo/

# Then check for metadata-related functionality in sozo
rg -A 5 "metadata" crates/sozo/ops/src/

Length of output: 13032


Line range hint 1-50: Verify impact of removing ipfs-api-backend-hyper, sensei

The removal of ipfs-api-backend-hyper from dev-dependencies might affect metadata-related tests. Let's ensure this doesn't impact the metadata upload functionality.

✅ Verification successful

The removal of ipfs-api-backend-hyper dependency is safe

The IPFS functionality in sozo-ops is handled through the dojo-world crate's metadata feature, which we can see is properly configured in the dependencies. The IPFS client dependency was only needed in dev-dependencies for testing, and those tests are now properly handled in dojo-world's metadata tests.

  • The only IPFS references in sozo-ops are:
    • A comment noting that IPFS testing is handled in dojo-world
    • A function doc comment describing metadata upload functionality
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining IPFS-related functionality that might be affected

# Search for IPFS-related imports or usage
rg -i "ipfs|ipfs_api"

# Look for any test files that might need the IPFS backend
fd -e rs test_ --exec rg -l "ipfs"

Length of output: 11950


Script:

#!/bin/bash
# Let's check if sozo-ops has any direct IPFS functionality or tests that might be affected

# Check for IPFS-related code in sozo-ops
rg -i "ipfs" crates/sozo/ops/ -A 5

# Check if sozo-ops has any dependencies on IPFS-related crates in its dependency tree
cargo tree -p sozo-ops | rg -i "ipfs"

# Check if there are any IPFS-related imports in sozo-ops
ast-grep --pattern 'use $_ :: $$$ipfs$$$'

Length of output: 1233

crates/dojo/world/src/metadata/ipfs.rs (2)

12-14: LGTM! Well-structured client wrapper

The struct provides good encapsulation of the underlying IPFS client implementation.


41-49: LGTM! Well-implemented test utility

The test-only get method is well-implemented with proper URI handling and error propagation.

crates/dojo/world/src/remote/mod.rs (2)

26-27: Ohayo! The metadata_hash field addition looks good, sensei!

The new field is well-documented and properly integrated into the struct. The use of Felt type is appropriate for Starknet compatibility.


26-27: Verify metadata_hash usage across the codebase

Ohayo sensei! Let's ensure the new field is properly utilized in related components.

✅ Verification successful

metadata_hash field is properly integrated across the codebase

Ohayo sensei! After reviewing the implementation, I can confirm that the metadata_hash field is well-integrated:

  • Properly initialized in resource creation (default: Felt::ZERO)
  • Has getter/setter methods in ResourceRemote
  • Correctly handled in MetadataUpdate events
  • Properly tracked in ResourceDiff for state management
  • Used in test assertions verifying the update functionality
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for metadata_hash usage in related files
# Expected: Find references in events_to_remote.rs and other related files

echo "Checking metadata_hash usage in related files..."
rg -l "metadata_hash" ./crates/dojo/world/src/

echo "Checking for potential metadata update handlers..."
ast-grep --pattern 'fn $name($$$) {
    $$$
    metadata_hash
    $$$
}'

Length of output: 678


Script:

#!/bin/bash
# Let's check the actual usage context of metadata_hash in these files

echo "Checking metadata_hash usage in remote/resource.rs..."
rg "metadata_hash" ./crates/dojo/world/src/remote/resource.rs -B 2 -A 2

echo -e "\nChecking metadata_hash usage in metadata/mod.rs..."
rg "metadata_hash" ./crates/dojo/world/src/metadata/mod.rs -B 2 -A 2

echo -e "\nChecking metadata_hash usage in remote/events_to_remote.rs..."
rg "metadata_hash" ./crates/dojo/world/src/remote/events_to_remote.rs -B 2 -A 2

echo -e "\nChecking metadata_hash usage in diff/resource.rs..."
rg "metadata_hash" ./crates/dojo/world/src/diff/resource.rs -B 2 -A 2

echo -e "\nChecking metadata_hash usage in diff/compare.rs..."
rg "metadata_hash" ./crates/dojo/world/src/diff/compare.rs -B 2 -A 2

Length of output: 4078

bin/sozo/src/commands/migrate.rs (1)

78-79: Ohayo! The metadata upload implementation looks solid, sensei! 🎋

The addition of metadata upload functionality after migration but before manifest writing is well-placed and properly handles errors. This change effectively addresses the issue where metadata wouldn't be uploaded after failed deployments.

However, let's verify the error recovery scenarios to ensure robustness.

✅ Verification successful

Let me gather more information about the metadata upload implementation and error handling.


Ohayo! The metadata upload implementation is well-protected, sensei! 🎋

The implementation shows robust error handling with proper context messages. The codebase includes:

  • Comprehensive test coverage for metadata upload in dojo/world/src/metadata/metadata_test.rs
  • Error recovery through the retry macro in saya/core/src/macros.rs with up to 20 retries
  • Proper error propagation with descriptive context messages

The metadata upload during migration is correctly placed and follows the established error handling patterns in the codebase.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for error handling patterns in metadata upload scenarios
# Expected: Find test cases covering metadata upload failures and recovery

# Look for test cases related to metadata upload failures
rg -A 10 "upload_metadata.*fail|fail.*upload_metadata" --type rust

# Look for error recovery scenarios in tests
ast-grep --pattern 'fn test_$_metadata_upload($_) {
  $$$
}'

Length of output: 1847


Script:

#!/bin/bash
# Search for metadata upload implementation and error handling
ast-grep --pattern 'impl $_ {
  $$$
  upload_metadata($$$) {
    $$$
  }
  $$$
}'

# Look for retry mechanisms or error recovery patterns
rg -A 5 "retry|recover|fallback" --type rust

# Find test cases that might cover metadata upload
rg -A 10 "test.*metadata|metadata.*test" --type rust

Length of output: 23182

crates/dojo/core-cairo-test/src/tests/world/metadata.cairo (3)

12-12: LGTM! The metadata initialization looks good, sensei!

The new metadata_hash field is properly initialized with a test value of 42.


74-74: Consistent metadata initialization across negative test cases!

The metadata_hash field is consistently initialized across all test cases, including permission checks and security scenarios. This ensures thorough validation of the new field under various error conditions.

Also applies to: 89-89, 116-116


Line range hint 33-49: Ohayo! The metadata event validation is thorough!

The test properly verifies all metadata fields, including the new metadata_hash. The event assertion at line 49 ensures the hash is correctly propagated through the system.

Let's verify the event structure matches the implementation:

✅ Verification successful

Ohayo! The metadata event validation is perfectly aligned with the contract implementation, sensei!

The test correctly validates all fields of the MetadataUpdate event:

  • resource field matches the contract's felt252 type
  • uri field matches the contract's ByteArray type
  • hash field matches the contract's felt252 type

The event structure in the world contract exactly matches what's being tested, including the order and types of all fields.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify MetadataUpdate event structure in the world contract
ast-grep --pattern 'enum Event {
  $$$
  MetadataUpdate: MetadataUpdate,
  $$$
}'

Length of output: 83


Script:

#!/bin/bash
# Let's try a different approach to find the event structure
rg -A 5 "struct MetadataUpdate" 

Length of output: 1451


Script:

#!/bin/bash
# Also search for the event enum definition with a more flexible pattern
rg -A 10 "enum Event \{"

Length of output: 28301


Script:

#!/bin/bash
# Search for files that might contain the event definition
fd "world\.cairo$"

Length of output: 212


Script:

#!/bin/bash
# Let's check the world contract for MetadataUpdate event
rg -A 5 "MetadataUpdate" crates/dojo/core/src/world/world_contract.cairo

Length of output: 705


Script:

#!/bin/bash
# Also check the event struct definition
rg -B 5 -A 5 "struct MetadataUpdate" crates/dojo/core/src/world/world_contract.cairo

Length of output: 334

crates/dojo/world/src/uri.rs (1)

1-1: Ohayo! Clean import addition, sensei!

The new import for Hash and Hasher traits is well-placed and necessary for the Hash implementation.

crates/dojo/world/src/diff/resource.rs (1)

116-123: Verify metadata upload integration

Let's verify that this implementation properly integrates with the metadata upload logic in other files.

✅ Verification successful

Implementation verified and properly integrated, sensei! ✨

The metadata_hash implementation in resource.rs is correctly integrated with the metadata upload system. The code shows:

  • In migrate/mod.rs, the hash is used to compare current and new metadata states before uploads
  • In remote/resource.rs, there's proper hash management with getters/setters
  • In metadata/mod.rs, there's hash computation support

The implementation aligns perfectly with the PR's goal of uploading metadata only when changed, as evidenced by the comparison logic in the migration module.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Check for metadata upload implementation that would use this hash
echo "Searching for metadata upload implementation..."
rg -l "metadata_hash" --type rust

# Look for any tests involving metadata hash comparison
echo "Searching for related tests..."
rg -l "test.*metadata.*hash" --type rust

Length of output: 649


Script:

#!/bin/bash
# Let's check how metadata_hash is used in the implementation files
echo "Checking metadata_hash usage in remote/resource.rs..."
rg "metadata_hash" -B 2 -A 2 crates/dojo/world/src/remote/resource.rs

echo "\nChecking metadata_hash usage in metadata/mod.rs..."
rg "metadata_hash" -B 2 -A 2 crates/dojo/world/src/metadata/mod.rs

echo "\nChecking metadata_hash usage in migrate/mod.rs..."
rg "metadata_hash" -B 2 -A 2 crates/sozo/ops/src/migrate/mod.rs

# Let's also check for any metadata comparison logic
echo "\nChecking for metadata comparison implementations..."
ast-grep --pattern 'fn $FUNC_NAME($$$) {
    $$$
    metadata_hash
    $$$
}'

Length of output: 3101

crates/sozo/ops/src/tests/migration.rs (3)

8-8: Ohayo! The new imports look good, sensei!

The addition of ResourceConfig from dojo_world::config aligns well with the PR's metadata handling objectives.

Also applies to: 16-16


75-75: Excellent backward compatibility maintenance, sensei!

The existing tests have been properly updated to pass false for with_metadata, maintaining their original behavior while accommodating the new feature.

Also applies to: 84-87


42-42: Clean implementation of the metadata upload feature, sensei!

The addition of the with_metadata parameter and conditional upload logic aligns perfectly with the PR objectives. The error handling is robust with descriptive messages.

Let's verify the error handling behavior:

Also applies to: 63-69

✅ Verification successful

Let me analyze the error handling patterns in the migration-related code to verify the implementation.


Clean error handling implementation confirmed, sensei!

The migrate_spawn_and_move function properly propagates errors through the MigrationResult type, which is consistently used across the codebase. The error handling is robust with:

  • Proper error propagation using the ? operator
  • Structured result type containing both success status and manifest
  • Clear separation between metadata and migration concerns
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other error handling patterns in migration-related code
rg -A 2 "expect|unwrap|context" crates/sozo/ops/src/

Length of output: 25177


Script:

#!/bin/bash
# Search for error handling in migration.rs and related files
rg -A 5 "migrate_spawn_and_move|MigrationResult" crates/sozo/ops/src/

Length of output: 3783

crates/dojo/world/src/diff/compare.rs (2)

Line range hint 29-39: Similar metadata comparison needed for Model and Event implementations, sensei

The ModelLocal and EventLocal implementations also need to be updated to handle metadata changes consistently.

Let's verify if there are any other implementations we might have missed:

Also applies to: 41-51


Line range hint 53-63: Review namespace metadata handling strategy

The NamespaceLocal comparison uses unreachable!() for updates. Should we consider allowing metadata updates for namespaces as well?

Let's check if namespaces have metadata in other parts of the codebase:

✅ Verification successful

Let's search for more context about namespace structures and metadata handling.


Ohayo sensei! After analyzing the codebase, I can now provide a definitive response about the namespace metadata concern.

The unreachable!() for namespace updates is correct and should be maintained

Looking at the struct definitions, we can see that NamespaceLocal only contains a name field, while NamespaceRemote has name, owners, and writers. The comparison logic is intentionally strict because:

  • Namespaces are fundamental identity containers that shouldn't change names after creation
  • Permission changes (owners/writers) are handled separately through dedicated flows
  • There is no additional metadata to sync between local and remote representations
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for namespace metadata references
rg -A 3 'namespace.*metadata' 

Length of output: 1296


Script:

#!/bin/bash
# Search for namespace struct definitions and implementations
ast-grep --pattern 'struct Namespace {
  $$$
}'

# Also check for NamespaceLocal struct
ast-grep --pattern 'struct NamespaceLocal {
  $$$
}'

# Look for any metadata fields in these structs
rg -B 2 -A 5 'struct (Namespace|NamespaceLocal)'

Length of output: 4690

crates/dojo/world/src/config/profile_config.rs (1)

12-12: Ohayo! Clean import addition.

The ResourceConfig import is well-placed and properly organized with other local imports.

crates/dojo/world/src/remote/resource.rs (2)

179-197: The new metadata hash methods look well-implemented, sensei!

The implementation follows the established patterns in the codebase and properly handles all resource types. The special handling of Namespace returning Felt::ZERO is consistent with similar methods in the file.

Let's verify the test coverage for these new methods:

#!/bin/bash
# Description: Check for test coverage of the new metadata hash methods
# Look for test files that exercise these methods

# Search for test files that might contain metadata hash related tests
echo "=== Checking for metadata hash tests ==="
rg -l "metadata_hash" "$(fd -t f -e rs test)"

# Look for specific test cases
echo "=== Checking for specific test cases ==="
rg "set_metadata_hash|metadata_hash" "$(fd -t f -e rs test)"

33-34: Ohayo! The metadata_hash field implementation looks solid, sensei!

The addition of metadata_hash with proper initialization to Felt::ZERO aligns well with the PR objectives for tracking metadata changes. Let's verify that all constructors properly initialize this field.

Also applies to: 85-85

crates/dojo/world/src/diff/mod.rs (2)

32-33: Ohayo! The metadata_hash field addition aligns with PR objectives

The addition of metadata_hash to WorldStatusInfo enables tracking metadata changes between local and remote worlds, which directly supports the PR's goal of selective metadata uploads.


123-123: Verify metadata hash propagation in remote world

The code correctly propagates the remote world's metadata hash, but let's verify there are no edge cases where this hash might be uninitialized or invalid.

✅ Verification successful

Let me gather more information about the metadata hash initialization and usage.


Metadata hash handling is properly implemented, sensei! ✨

The code analysis shows that metadata_hash is handled safely throughout the codebase:

  • It's properly propagated through WorldRemote for Contract, Model, and Event resources
  • Namespace resources explicitly return Felt::ZERO as their metadata hash
  • Tests verify the metadata hash updates and propagation
  • The hash is properly serialized/deserialized in the contract ABIs

The current implementation in the diff module correctly uses the remote world's metadata hash, which is consistent with the rest of the codebase's handling.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for potential uninitialized metadata hash cases
# Look for places where WorldRemote is constructed without setting metadata_hash

# Search for WorldRemote struct construction
ast-grep --pattern 'WorldRemote {
  $$$
  metadata_hash: $_,
  $$$
}'

# Search for potential metadata_hash assignments
rg -A 3 'metadata_hash\s*=' 

Length of output: 1523


Script:

#!/bin/bash
# Check for WorldRemote struct definition and initialization
ast-grep --pattern 'struct WorldRemote {
  $$$
}'

# Look for metadata_hash field initialization in constructors/new functions
ast-grep --pattern 'fn new($$$) -> WorldRemote {
  $$$
}'

# Search for any direct metadata_hash field access
rg -A 2 '\.metadata_hash'

Length of output: 4997

crates/dojo/world/src/remote/events_to_remote.rs (1)

58-58: LGTM! Event selector properly added

Ohayo! The MetadataUpdate event selector is correctly added to enable filtering of metadata update events.

crates/dojo/core/src/world/world_contract.cairo (2)

114-115: Ohayo! The MetadataUpdate event structure enhancement looks good, sensei!

The addition of the hash field enables efficient tracking of metadata changes, which directly addresses the PR objectives for uploading metadata only when changed.


360-364: The MetadataUpdate event emission is properly implemented!

The event emission correctly includes all fields including the new hash field, maintaining consistency with the event structure.

crates/dojo/world/src/contracts/abigen/world.rs (4)

Line range hint 394-428: Ohayo! The MetadataUpdate struct changes look good, sensei!

The addition of the hash field to track metadata changes is well implemented with proper serialization and deserialization support. This change enables tracking when metadata actually changes, which aligns with the PR objective.


Line range hint 632-665: The ResourceMetadata struct changes are solid, sensei!

The addition of the metadata_hash field provides a way to track the state of metadata, complementing the MetadataUpdate changes. The serialization and deserialization implementations are correct.


1804-1815: Event handling updates are properly implemented, sensei!

The MetadataUpdate event deserialization has been correctly updated to handle the new hash field in both TryFrom trait implementations. The error handling and deserialization logic are consistent.

Also applies to: 2684-2695


394-396: Verify consistent usage of hash fields across the codebase

Let's ensure the new hash fields are used consistently in all metadata-related operations.

Also applies to: 632-633

✅ Verification successful

Based on the search results, I can see that the hash field is consistently used across the codebase:

  1. The ResourceMetadata struct in world.rs properly includes the metadata_hash field
  2. The metadata hash is properly handled in metadata updates through MetadataUpdate events
  3. The hash field is consistently used in remote resources tracking via metadata_hash field
  4. There are proper test cases verifying metadata hash updates
  5. The hash field is properly integrated with the world state diff tracking

The hash field implementation is consistent across the codebase

The changes are properly integrated with all metadata-related operations including:

  • Resource metadata storage
  • Metadata updates and events
  • Remote state tracking
  • World state diffing
  • Test coverage
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent usage of metadata hash fields
# Expected: All metadata-related code should handle the hash fields

# Search for metadata-related structs and their usage
rg -A 5 "struct.*Metadata|fn.*metadata" --type rust

# Search for hash field usage in metadata context
rg -A 3 "metadata.*hash|hash.*metadata" --type rust

Length of output: 42698

crates/dojo/world/src/metadata/mod.rs (1)

34-35: Ensure Felt conversion is correct and handles large hashes

Ohayo, sensei! When converting new_hash (a u64) into a Felt using Felt::from_raw([0, 0, 0, new_hash]), ensure that the hash value fits within the field and that the ordering of the array elements correctly represents the intended value.

Let's verify if the conversion aligns with the expected behavior. Consider running a test to confirm that the Felt value accurately represents the u64 hash.

crates/dojo/world/src/metadata/metadata_test.rs (1)

1-192: Ohayo, sensei! The tests are well-structured and comprehensive

The implementation of test_world_metadata and test_resource_metadata effectively verifies the upload and retrieval of metadata. The use of helper functions like build_world_metadata and assert_ipfs_content enhances readability and maintainability.

crates/sozo/ops/src/migrate/mod.rs (2)

109-164: Function upload_metadata is well-implemented

Ohayo, sensei! The new upload_metadata method properly handles metadata uploads for the world, contracts, models, and events. The integration with the invoker pattern and conditional multicall execution is efficient.


121-121: Verify the correctness of using Felt::ZERO for the world resource ID

Ohayo, sensei! Ensure that Felt::ZERO is indeed the correct identifier for the world resource when retrieving and updating metadata. Verifying this helps prevent potential mismatches or errors.

crates/dojo/world/src/config/resource_config.rs Outdated Show resolved Hide resolved
crates/dojo/world/Cargo.toml Show resolved Hide resolved
crates/dojo/world/src/metadata/ipfs.rs Outdated Show resolved Hide resolved
Comment on lines 8 to 10
const IPFS_CLIENT_URL: &str = "https://ipfs.infura.io:5001";
const IPFS_USERNAME: &str = "2EBrzr7ZASQZKH32sl2xWauXPSA";
const IPFS_PASSWORD: &str = "12290b883db9138a8ae3363b6739d220";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo! Critical security concern: Remove hardcoded credentials

The IPFS credentials should not be hardcoded in the source code as this poses a significant security risk. These credentials could be compromised if the repository is public.

Consider this approach instead:

-const IPFS_CLIENT_URL: &str = "https://ipfs.infura.io:5001";
-const IPFS_USERNAME: &str = "2EBrzr7ZASQZKH32sl2xWauXPSA";
-const IPFS_PASSWORD: &str = "12290b883db9138a8ae3363b6739d220";
+const IPFS_CLIENT_URL: &str = env!("IPFS_CLIENT_URL", "IPFS client URL not set");
+const IPFS_USERNAME: &str = env!("IPFS_USERNAME", "IPFS username not set");
+const IPFS_PASSWORD: &str = env!("IPFS_PASSWORD", "IPFS password not set");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const IPFS_CLIENT_URL: &str = "https://ipfs.infura.io:5001";
const IPFS_USERNAME: &str = "2EBrzr7ZASQZKH32sl2xWauXPSA";
const IPFS_PASSWORD: &str = "12290b883db9138a8ae3363b6739d220";
const IPFS_CLIENT_URL: &str = env!("IPFS_CLIENT_URL", "IPFS client URL not set");
const IPFS_USERNAME: &str = env!("IPFS_USERNAME", "IPFS username not set");
const IPFS_PASSWORD: &str = env!("IPFS_PASSWORD", "IPFS password not set");

crates/dojo/world/src/uri.rs Show resolved Hide resolved
crates/dojo/world/src/metadata/mod.rs Outdated Show resolved Hide resolved
crates/sozo/ops/src/migrate/mod.rs Outdated Show resolved Hide resolved
remybar added a commit to remybar/book that referenced this pull request Nov 17, 2024
Update the Metadata page according to dojoengine/dojo#2691.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (12)
crates/dojo/world/src/metadata/mod.rs (1)

27-45: Enhance error handling with specific error types

Ohayo! While using anyhow::Result is convenient, it might be better to define specific error types for different failure scenarios:

  • IPFS upload failures
  • File reading errors
  • Hash computation errors

Consider creating a custom error enum:

#[derive(Debug, thiserror::Error)]
pub enum MetadataError {
    #[error("Failed to upload to IPFS: {0}")]
    IpfsUpload(#[from] ipfs::Error),
    #[error("Failed to read file: {0}")]
    FileRead(#[from] std::io::Error),
    #[error("Hash computation failed: {0}")]
    HashComputation(String),
}

pub type Result<T> = std::result::Result<T, MetadataError>;
crates/dojo/world/src/config/metadata_config.rs (1)

39-96: Ohayo, sensei! Consider implementing a common trait for file hashing

Since both WorldMetadata and ResourceMetadata share similar file hashing logic, consider extracting this functionality into a common trait to promote code reuse and ensure consistent error handling across all implementations.

Example trait design:

trait FileHasher {
    fn hash_file<H: Hasher>(&self, path: &std::path::Path, state: &mut H) {
        if let Ok(data) = std::fs::read(path) {
            data.hash(state);
        } else {
            eprintln!("Warning: Unable to read file for hashing: {}", path.display());
        }
    }
}
crates/sozo/ops/src/tests/migration.rs (3)

42-42: Consider enhancing error handling in metadata upload.

Ohayo sensei! While the implementation looks good, the error messages could be more descriptive to help with debugging.

Consider this enhancement:

-    let res = migration.migrate(&mut ui).await.expect("Migration spawn-and-move failed.");
+    let res = migration.migrate(&mut ui).await
+        .context("Failed to migrate spawn-and-move world")?;

     if with_metadata {
-        migration.upload_metadata(&mut ui).await.expect("Upload metadata failed");
+        migration.upload_metadata(&mut ui).await
+            .context("Failed to upload world metadata after successful migration")?;
     }

Also applies to: 63-69


89-111: Add documentation for the check_resources helper function.

Ohayo! The helper function is well-implemented, but it would benefit from documentation explaining:

  • Purpose and usage
  • Parameter descriptions
  • Expected behavior of the checker function

Consider adding this documentation:

+/// Validates metadata for a list of resources against expected criteria
+/// 
+/// # Arguments
+/// * `diff` - The WorldDiff containing the resources to check
+/// * `resources` - Optional list of ResourceConfig to validate
+/// * `expected_count` - Expected number of resources
+/// * `checker` - Function to validate the metadata hash of each resource
 fn check_resources(
     diff: &WorldDiff,
     resources: Option<Vec<ResourceConfig>>,
     expected_count: usize,
     checker: &dyn Fn(Felt) -> bool,
 ) {

113-150: Test implementation is thorough but could be more explicit.

Ohayo sensei! The test covers the key scenarios well, but consider these enhancements:

  1. Add explicit assertions for specific metadata values, not just presence/absence
  2. Test error cases (e.g., failed uploads)

Also, the test aligns perfectly with the PR objectives mentioned in #2096, ensuring metadata can be uploaded after initial deployment.

crates/dojo/world/src/metadata/metadata_test.rs (2)

14-64: Ohayo! Consider improving test data handling, sensei.

The helper functions could be more robust with these improvements:

  1. Extract hardcoded file paths into constants
  2. Add validation for social media URLs
  3. Consider using a test fixture or configuration file for test data

Here's a suggested improvement:

+ const TEST_DATA_DIR: &str = "./src/metadata/metadata_test_data";
+ const TEST_COVER_PATH: &str = "./src/metadata/metadata_test_data/cover.png";
+ const TEST_ICON_PATH: &str = "./src/metadata/metadata_test_data/icon.png";

fn build_world_metadata() -> WorldMetadata {
-    cover_uri: Some(Uri::File(
-        fs::canonicalize(
-            PathBuf::from_str("./src/metadata/metadata_test_data/cover.png").unwrap(),
-        )
-        .unwrap(),
-    )),
+    cover_uri: Some(Uri::File(
+        fs::canonicalize(PathBuf::from_str(TEST_COVER_PATH).unwrap())
+            .expect("Test cover image not found")
+    )),

66-138: Ohayo! Great test coverage, but let's make it even better, sensei!

The test thoroughly validates the happy path, but consider adding:

  1. Negative test cases (invalid URIs, network failures)
  2. Edge cases (empty fields, very large metadata)

Regarding the TODO on line 137:

Would you like me to help implement a mock IpfsClient for more reliable testing? This would:

  • Remove external IPFS dependency
  • Allow testing network failure scenarios
  • Speed up test execution
crates/dojo/world/src/diff/compare.rs (1)

Line range hint 19-29: Ohayo! Critical issue: Metadata changes are not detected in comparison logic

The comparison implementations for ContractLocal, ModelLocal, and EventLocal only check class_hash but ignore metadata_hash changes. This doesn't align with the PR's objective of properly handling metadata updates.

Here's how to fix the ContractLocal implementation (apply similar changes to ModelLocal and EventLocal):

 impl ComparableResource for ContractLocal {
     fn compare(self, remote: ResourceRemote) -> ResourceDiff {
         let remote_contract = remote.as_contract_or_panic();
 
-        if self.common.class_hash == remote_contract.common.current_class_hash() {
+        if self.common.class_hash == remote_contract.common.current_class_hash() 
+            && self.common.metadata_hash == remote_contract.common.metadata_hash {
             ResourceDiff::Synced(ResourceLocal::Contract(self), remote)
         } else {
             ResourceDiff::Updated(ResourceLocal::Contract(self), remote)
         }
     }
 }

Also applies to: 30-40, 41-51

crates/dojo/world/src/config/profile_config.rs (2)

21-23: Consider adding field-level documentation, sensei!

While the struct is well-documented, adding field-level documentation for the new metadata fields would help users understand their purpose and usage better.

Example addition:

 #[derive(Debug, Clone, Default, Deserialize)]
 pub struct ProfileConfig {
     pub world: WorldConfig,
+    /// Metadata configurations for models in the world
     pub models: Option<Vec<ResourceConfig>>,
+    /// Metadata configurations for contracts in the world
     pub contracts: Option<Vec<ResourceConfig>>,
+    /// Metadata configurations for events in the world
     pub events: Option<Vec<ResourceConfig>>,

212-231: Consider adding edge case tests, sensei!

While the happy path is well tested, consider adding tests for:

  • Empty vectors for models/contracts/events
  • Invalid URI formats
  • Duplicate tags within the same resource type

Example test case:

#[test]
fn test_profile_config_empty_resources() {
    let content = r#"
    [world]
    name = "test"
    seed = "abcd"

    [namespace]
    default = "test"

    [[models]]
    [[contracts]]
    [[events]]
    "#;

    let config = toml::from_str::<ProfileConfig>(content).unwrap();
    assert!(config.models.unwrap().is_empty());
    assert!(config.contracts.unwrap().is_empty());
    assert!(config.events.unwrap().is_empty());
}
crates/sozo/ops/src/migrate/mod.rs (2)

109-164: Ohayo, sensei! Ensure calls exist before invoking

To prevent unnecessary operations when there are no metadata changes, consider checking if there are any invocations before performing multicall or sequential calls. This enhances efficiency and avoids redundant actions.

Apply this diff to add the check:

     if !invoker.calls.is_empty() {
+        if self.do_multicall() {
             ui.update_text_boxed(format!("Uploading {} metadata...", invoker.calls.len()));
             invoker.multicall().await.map_err(|e| anyhow!(e.to_string()))?;
         } else {
             ui.update_text_boxed(format!(
                 "Uploading {} metadata (sequentially)...",
                 invoker.calls.len()
             ));
             invoker.invoke_all_sequentially().await.map_err(|e| anyhow!(e.to_string()))?;
         }
+    } else {
+        ui.update_text("No metadata changes detected. Skipping upload.");
+    }

166-192: Ohayo, sensei! Parallelize metadata uploads for better performance

The metadata uploads in upload_metadata_from_resource_config are currently processed sequentially. To optimize performance, especially with numerous resources, consider parallelizing the uploads using asynchronous tasks.

You can refactor the function to process uploads concurrently:

async fn upload_metadata_from_resource_config(
    &self,
    config: &[ResourceConfig],
) -> anyhow::Result<Vec<Call>> {
    let futures = config.iter().map(|item| async {
        let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag);
        let current_hash = self
            .diff
            .resources
            .get(&selector)
            .map_or(Felt::ZERO, |r| r.metadata_hash());
        let new_metadata = metadata_config::ResourceMetadata::from(item.clone());
        let res = new_metadata.upload_if_changed(current_hash).await?;

        if let Some((new_uri, new_hash)) = res {
            Ok(Some(self.world.set_metadata_getcall(&ResourceMetadata {
                resource_id: selector,
                metadata_uri: ByteArray::from_string(&new_uri)?,
                metadata_hash: new_hash,
            })))
        } else {
            Ok(None)
        }
    });

    let results = futures::future::join_all(futures).await;
    let mut calls = Vec::new();

    for result in results {
        if let Ok(Some(call)) = result {
            calls.push(call);
        }
    }

    Ok(calls)
}
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 091964a and e8a1bac.

⛔ Files ignored due to path filters (4)
  • Cargo.lock is excluded by !**/*.lock
  • crates/dojo/world/src/metadata/metadata_test_data/icon.png is excluded by !**/*.png
  • spawn-and-move-db.tar.gz is excluded by !**/*.gz
  • types-test-db.tar.gz is excluded by !**/*.gz
📒 Files selected for processing (27)
  • bin/sozo/src/commands/migrate.rs (1 hunks)
  • crates/dojo/core-cairo-test/src/tests/world/metadata.cairo (6 hunks)
  • crates/dojo/core/src/model/metadata.cairo (1 hunks)
  • crates/dojo/core/src/world/world_contract.cairo (2 hunks)
  • crates/dojo/world/Cargo.toml (1 hunks)
  • crates/dojo/world/src/config/metadata_config.rs (2 hunks)
  • crates/dojo/world/src/config/mod.rs (1 hunks)
  • crates/dojo/world/src/config/profile_config.rs (4 hunks)
  • crates/dojo/world/src/config/resource_config.rs (1 hunks)
  • crates/dojo/world/src/contracts/abigen/world.rs (8 hunks)
  • crates/dojo/world/src/diff/compare.rs (3 hunks)
  • crates/dojo/world/src/diff/mod.rs (3 hunks)
  • crates/dojo/world/src/diff/resource.rs (1 hunks)
  • crates/dojo/world/src/metadata/ipfs.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test_data/abi.json (0 hunks)
  • crates/dojo/world/src/metadata/metadata_test_data/source.cairo (0 hunks)
  • crates/dojo/world/src/metadata/mod.rs (1 hunks)
  • crates/dojo/world/src/metadata/world.rs (0 hunks)
  • crates/dojo/world/src/remote/events_to_remote.rs (3 hunks)
  • crates/dojo/world/src/remote/mod.rs (1 hunks)
  • crates/dojo/world/src/remote/resource.rs (3 hunks)
  • crates/dojo/world/src/uri.rs (3 hunks)
  • crates/sozo/ops/Cargo.toml (1 hunks)
  • crates/sozo/ops/src/migrate/mod.rs (2 hunks)
  • crates/sozo/ops/src/tests/migration.rs (4 hunks)
  • examples/spawn-and-move/dojo_dev.toml (2 hunks)
💤 Files with no reviewable changes (3)
  • crates/dojo/world/src/metadata/metadata_test_data/abi.json
  • crates/dojo/world/src/metadata/metadata_test_data/source.cairo
  • crates/dojo/world/src/metadata/world.rs
🚧 Files skipped from review as they are similar to previous changes (15)
  • bin/sozo/src/commands/migrate.rs
  • crates/dojo/core-cairo-test/src/tests/world/metadata.cairo
  • crates/dojo/core/src/model/metadata.cairo
  • crates/dojo/core/src/world/world_contract.cairo
  • crates/dojo/world/Cargo.toml
  • crates/dojo/world/src/config/mod.rs
  • crates/dojo/world/src/config/resource_config.rs
  • crates/dojo/world/src/diff/mod.rs
  • crates/dojo/world/src/diff/resource.rs
  • crates/dojo/world/src/metadata/ipfs.rs
  • crates/dojo/world/src/remote/events_to_remote.rs
  • crates/dojo/world/src/remote/mod.rs
  • crates/dojo/world/src/remote/resource.rs
  • crates/sozo/ops/Cargo.toml
  • examples/spawn-and-move/dojo_dev.toml
🔇 Additional comments (20)
crates/dojo/world/src/metadata/mod.rs (1)

69-84: 🛠️ Refactor suggestion

Extract common functionality and maintain consistency

Ohayo! There's significant code duplication between WorldMetadata and ResourceMetadata implementations. Also, ResourceMetadata handles only icon_uri while WorldMetadata handles both icon and cover.

Consider extracting common functionality:

async fn upload_file_to_ipfs(
    file_path: &Path,
    ipfs_client: &IpfsClient
) -> Result<String> {
    if !file_path.exists() {
        return Err(anyhow!("File not found: {}", file_path.display()));
    }
    let data = std::fs::read(file_path)?;
    let uri = ipfs_client.upload(data).await?;
    // Verify upload
    let verified_data = ipfs_client.cat(&uri).await?;
    if verified_data != data {
        return Err(anyhow!("Upload verification failed"));
    }
    Ok(uri)
}

#[async_trait]
impl MetadataStorage for ResourceMetadata {
    async fn upload(&self, ipfs_client: &IpfsClient) -> Result<String> {
        let mut meta = self.clone();

        if let Some(Uri::File(icon)) = &self.icon_uri {
            meta.icon_uri = Some(Uri::Ipfs(upload_file_to_ipfs(icon, ipfs_client).await?));
        };

        let serialized = json!(meta).to_string();
        ipfs_client.upload(serialized).await
    }
}

Let's verify if ResourceMetadata should handle cover_uri:

crates/dojo/world/src/config/metadata_config.rs (4)

48-48: Ohayo, sensei! Previous review comment about HashMap hashing is still valid

The current implementation of hashing the socials HashMap could produce inconsistent hashes due to non-deterministic key ordering. Please refer to the previous review comment for the solution that involves sorting keys before hashing.


52-60: Ohayo, sensei! Previous review comment about error handling is still valid

The use of expect() for file operations can cause panics. Please refer to the previous review comment for a more graceful error handling solution.


64-81: Ohayo, sensei! Clean implementation of ResourceMetadata

The struct and its From implementation are well-designed with clear documentation and appropriate use of the naming utility.


91-94: Ohayo, sensei! Previous review comment about error handling applies here too

The same file reading error handling issue exists in the ResourceMetadata Hash implementation. Please refer to the previous review comment for the graceful error handling solution.

crates/dojo/world/src/uri.rs (2)

60-68: Ohayo! Clean Display implementation!

The Display implementation is well-crafted:

  • Safe handling of non-UTF8 paths using to_string_lossy()
  • Efficient direct formatting for URLs and IPFS URIs

Line range hint 21-68: Verify integration with metadata handling

Let's ensure the new Hash and Display implementations align with the metadata upload changes mentioned in the PR objectives.

✅ Verification successful

Ohayo sensei! After reviewing the implementation, I can now provide a definitive response about the Hash implementation for Uri.

Hash implementation is correctly aligned with serialization and metadata handling

The Hash implementation for Uri is consistent with its usage in metadata handling and follows best practices:

  1. The Hash implementation correctly handles all Uri variants (Http, Ipfs, File) by hashing their string representations
  2. This aligns with the serialization implementation which uses the same string representations
  3. The hashing is used properly in metadata handling through compute_metadata_hash function
  4. The implementation is used consistently across the codebase for resource metadata tracking
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify usage of Uri hashing in metadata-related code

# Search for metadata structs using Uri
rg --type rust "struct.*Metadata.*Uri" -C 5

# Search for metadata upload functions using Uri
rg --type rust "upload.*metadata.*Uri" -C 5

# Search for Hash derivations on metadata structs
ast-grep --pattern 'derive($$$, Hash, $$$)
struct $_Metadata'

Length of output: 155


Script:

#!/bin/bash
# Let's search for Uri usage in the codebase more broadly

# Find Uri struct definition and its usages
rg --type rust "enum Uri" -B 2 -A 5

# Search for metadata-related code that might use Uri
rg --type rust "Metadata" -B 2 -A 5

# Look for Uri serialization/deserialization usage
rg --type rust "Uri::from_string|Uri::serialize" -B 2 -A 5

# Search for tests involving Uri
rg --type rust "test.*Uri" -B 2 -A 5

Length of output: 112875


Script:

#!/bin/bash
# Let's search for the Uri implementation and its usage in metadata handling

# Find Uri implementation and its methods
rg --type rust "impl.*Uri" -B 2 -A 10

# Search for Uri usage in metadata-related code
rg --type rust "Uri.*metadata" -B 2 -A 5

# Look for Uri serialization tests
rg --type rust "test.*Uri.*serialize" -B 2 -A 5

# Search for metadata hash implementations
rg --type rust "metadata_hash" -B 2 -A 5

Length of output: 22749

crates/sozo/ops/src/tests/migration.rs (1)

8-8: LGTM! New imports align with metadata handling requirements.

Ohayo! The added imports for ResourceConfig and Felt are well-chosen for handling metadata and hash comparisons.

Also applies to: 16-16

crates/dojo/world/src/metadata/metadata_test.rs (1)

1-192: Ohayo! Overall excellent test coverage for the metadata changes, sensei!

The tests effectively validate the new metadata upload behavior, ensuring that:

  1. Metadata is uploaded only when changed
  2. Hashes are correctly tracked
  3. IPFS storage and retrieval work as expected

This aligns perfectly with the PR objectives to fix issues #2096 and #1907.

crates/dojo/world/src/diff/compare.rs (2)

111-111: Enhance test coverage for metadata changes, sensei!

The existing comment from the previous review is still valid. The tests include metadata_hash but don't verify its impact on comparison results. We need test cases that specifically verify metadata-only changes:

  1. Different metadata_hash but same class_hash
  2. Different metadata_hash and different class_hash
  3. Same metadata_hash but different class_hash

Also applies to: 147-147, 197-197


Line range hint 1-215: Verify metadata_hash field availability

Let's ensure the metadata_hash field exists in the required structures.

crates/dojo/world/src/config/profile_config.rs (3)

12-12: Ohayo! Import looks good, sensei!

The ResourceConfig import is properly organized with related configuration imports.


141-154: Test data structure looks good, sensei!

The TOML configuration test data is well-structured and covers the new metadata fields appropriately.


21-23: Verify metadata handling behavior, sensei!

The new fields enable better metadata management, aligning with PR objectives. Let's verify the metadata handling implementation:

✅ Verification successful

Based on the search results, I can see comprehensive metadata handling implementation across the codebase, particularly in the world configuration and resource management. The code shows proper metadata structures and upload functionality. Let me verify one more aspect about the actual metadata upload implementation:


Metadata implementation looks solid, sensei! ✨

The codebase shows robust metadata handling with:

  • Complete metadata structures for world and resources
  • Proper upload functionality with IPFS integration
  • Comprehensive test coverage including world metadata, resource metadata, and event handling
  • Consistent metadata storage and retrieval across the system

The new fields align perfectly with the existing metadata architecture and follow established patterns for resource configuration.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent metadata handling across the codebase

# Look for metadata-related functions that should handle these new fields
rg -A 5 "metadata" --type rust

# Check for any existing metadata upload implementations
ast-grep --pattern 'fn $upload_metadata($$$) {
  $$$
}'

Length of output: 152061


Script:

#!/bin/bash
# Check for metadata upload implementation in world config
ast-grep --pattern 'impl $_ {
  $$$
  pub async fn upload($$$) {
    $$$
  }
  $$$
}'

# Look for metadata storage/handling tests
rg "test.*metadata" --type rust

Length of output: 590

crates/dojo/world/src/contracts/abigen/world.rs (4)

396-396: Ohayo! Nice addition of the hash field for metadata change tracking.

The new hash field in the MetadataUpdate struct enables efficient tracking of metadata changes, which aligns perfectly with the PR's objective of uploading metadata only when necessary.


1804-1815: Ohayo! Clean implementation of metadata hash deserialization.

The deserialization logic for the new hash field follows best practices with proper error handling and consistent implementation patterns.


633-633: Ohayo! Let's verify the metadata hash handling.

The addition of metadata_hash to ResourceMetadata is a solid improvement. However, we should verify that this hash is being properly utilized in the metadata update process.

✅ Verification successful

Based on the search results, I can now generate the final response as the metadata hash implementation is well verified:

Ohayo! The metadata hash implementation looks solid, sensei!

The metadata_hash field is properly integrated across the codebase:

  • Used in change detection via compute_metadata_hash function
  • Properly serialized/deserialized in the Cairo contract interface
  • Correctly handled in resource updates through set_metadata_hash and metadata_hash methods
  • Verified by test cases checking hash state transitions
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check how metadata_hash is used in metadata updates

# Search for metadata hash comparisons
rg -A 3 "metadata_hash" --type rust

# Look for metadata update implementations
ast-grep --pattern 'fn $update_name($params) {
  $$$
  metadata_hash
  $$$
}'

Length of output: 10499


662-664: Sensei, let's ensure hash computation consistency.

While the deserialization implementation looks good, we should verify that the hash computation remains consistent across all code paths that update metadata.

✅ Verification successful

Let me generate additional scripts to check for hash computation implementation details.


Ohayo sensei! The hash computation is consistent and well-implemented.

The verification shows that:

  • There's a single, centralized compute_metadata_hash<T> function in crates/dojo/world/src/metadata/mod.rs that handles all metadata hash computations
  • It uses Rust's standard Hash trait and DefaultHasher, ensuring consistent hash computation across all types
  • The hash is properly converted to a Felt type with Felt::from_raw([0, 0, 0, hash])
  • All metadata hash updates flow through this single implementation
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent hash computation across the codebase

# Look for hash computation implementations
ast-grep --pattern 'fn $compute_name($params) {
  $$$
  hash
  $$$
}'

# Search for direct hash assignments
rg -A 3 "metadata_hash.*=" --type rust

Length of output: 10499


Script:

#!/bin/bash
# Check for hash computation implementation details

# Look for the compute_metadata_hash function implementation
ast-grep --pattern 'fn compute_metadata_hash<$_>($params) {
  $$$
}'

# Search for metadata hash computation related code
rg -A 5 "compute_metadata_hash" --type rust

# Look for any hash-related trait implementations
ast-grep --pattern 'impl Hash for $type {
  $$$
}'

Length of output: 1011

crates/sozo/ops/src/migrate/mod.rs (2)

23-23: Ohayo, sensei! Importing anyhow for enhanced error handling

Good job on importing anyhow::anyhow; it improves the consistency of error handling throughout the codebase.


109-164: Ohayo, sensei! Excellent integration of metadata upload functionality

The new upload_metadata method is well-structured and effectively integrates metadata uploading into the migration process. It enhances the robustness of the system by ensuring metadata consistency.

Comment on lines 17 to 25
/// Helper function to compute metadata hash using the Hash trait impl.
fn compute_metadata_hash<T>(data: T) -> u64
where
T: Hash,
{
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
hasher.finish()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Consider using a cryptographic hash function instead of DefaultHasher

Ohayo, sensei! The current implementation uses DefaultHasher which is designed for hash tables and not for cryptographic purposes. For metadata integrity verification, consider using a cryptographic hash function (e.g., blake3, sha3) to:

  1. Prevent potential hash collisions due to the limited 64-bit output
  2. Provide stronger security guarantees
  3. Better align with blockchain security requirements

Example implementation using blake3:

- use std::hash::{DefaultHasher, Hash, Hasher};
+ use blake3;
+ use std::hash::Hash;

- fn compute_metadata_hash<T>(data: T) -> u64
+ fn compute_metadata_hash<T>(data: T) -> [u8; 32]
  where
      T: Hash,
  {
-     let mut hasher = DefaultHasher::new();
-     data.hash(&mut hasher);
-     hasher.finish()
+     let mut hasher = blake3::Hasher::new();
+     data.hash(&mut hasher);
+     hasher.finalize().into()
  }

Committable suggestion skipped: line range outside the PR's diff.

crates/dojo/world/src/metadata/mod.rs Outdated Show resolved Hide resolved
crates/dojo/world/src/uri.rs Show resolved Hide resolved
crates/dojo/world/src/metadata/metadata_test.rs Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (13)
crates/dojo/world/src/metadata/metadata_service.rs (1)

1-4: Ohayo! The trait definition looks solid, sensei! 🎋

The trait is well-designed with appropriate marker traits (Send + Sync + Unpin) making it safe for concurrent usage. The use of anyhow::Result provides flexible error handling which is crucial for metadata operations that might fail.

Consider documenting the trait with rustdoc comments to explain:

  • The purpose of the service
  • The thread-safety guarantees
  • The relationship with IPFS
crates/dojo/world/src/metadata/fake_metadata_service.rs (3)

1-11: Ohayo! The imports and struct definition look solid, sensei!

The structure is clean and well-organized. However, it would be helpful to add documentation comments explaining the purpose of this fake service and its role in testing.

Consider adding documentation like this:

+/// A mock implementation of MetadataService for testing purposes.
+/// Stores metadata in memory using a HashMap instead of making actual IPFS calls.
 pub struct FakeMetadataService {
     data: HashMap<String, Vec<u8>>,
 }

13-17: Constructor looks good, sensei!

Consider adding the #[must_use] attribute since this function creates a new value.

 impl FakeMetadataService {
+    #[must_use]
     pub fn new() -> Self {
         Self { data: HashMap::new() }
     }
 }

32-35: Simplify the get() method implementation, sensei!

The current implementation can be made more idiomatic using cloned().

     #[cfg(test)]
     async fn get(&self, uri: String) -> Result<Vec<u8>> {
-        Ok(self.data.get(&uri).map(|x| x.clone()).unwrap_or(Vec::<u8>::new()))
+        Ok(self.data.get(&uri).cloned().unwrap_or_default())
     }
crates/dojo/world/src/metadata/metadata_test.rs (3)

16-52: Ohayo! Consider extracting common path handling logic, sensei.

The builder functions are well-structured, but there's repeated path handling logic. Consider creating a helper function:

fn test_file_path(filename: &str) -> PathBuf {
    fs::canonicalize(
        PathBuf::from_str(&format!("./src/metadata/metadata_test_data/{}", filename)).unwrap()
    ).unwrap()
}

Then use it in both builders:

icon_uri: Some(Uri::File(test_file_path("icon.png"))),

66-140: Consider adding edge cases for more robust testing, sensei.

The test has good coverage of the happy path and basic change detection. However, consider adding:

  1. Test with invalid/corrupted metadata
  2. Test with non-existent files
  3. Test concurrent uploads of the same metadata

Example for invalid metadata test:

#[tokio::test]
async fn test_world_metadata_invalid() {
    let mut metadata_service = FakeMetadataService::new();
    let mut invalid_metadata = build_world_metadata();
    invalid_metadata.cover_uri = Some(Uri::File(PathBuf::from("/nonexistent/path")));
    
    let res = invalid_metadata
        .upload_if_changed(&mut metadata_service, Felt::ZERO)
        .await;
    
    assert!(res.is_err());
}

142-197: Enhance assertions for resource metadata validation, sensei.

While the test structure is solid, consider strengthening the validation by:

  1. Adding assertions for the exact IPFS URI format (not just the prefix)
  2. Verifying the content-type of the uploaded icon
  3. Checking the size limits of the uploaded metadata

Example enhancement:

fn assert_ipfs_uri_format(uri: &str) {
    assert!(uri.starts_with("ipfs://"));
    assert_eq!(uri.len(), 46); // IPFS CIDv0 length
    assert!(uri[7..].chars().all(|c| c.is_ascii_hexdigit()));
}
crates/sozo/ops/src/migrate/mod.rs (1)

158-158: Consider enhancing error message

Ohayo! The error message could be more descriptive to help with debugging.

-            invoker.multicall().await.map_err(|e| anyhow!(e.to_string()))?;
+            invoker.multicall().await.map_err(|e| anyhow!("Failed to upload metadata via multicall: {}", e))?;
crates/dojo/world/src/metadata/ipfs_service.rs (2)

9-11: Consider Making IPFS Client Configurable

Hardcoding the IPFS client URL may limit flexibility across different environments.

Consider loading IPFS_CLIENT_URL from an environment variable or a configuration file to enhance configurability.


18-23: Enhance Error Handling in new Method

Errors during client initialization may lack sufficient context for debugging.

Consider adding more descriptive error messages to improve troubleshootability.

Apply this diff:

 impl IpfsMetadataService {
     pub fn new() -> Result<Self> {
         dotenv().ok();
         let client_url = env::var("IPFS_CLIENT_URL").unwrap_or_else(|_| "https://ipfs.infura.io:5001".to_string());
         let username = env::var("IPFS_USERNAME")?;
         let password = env::var("IPFS_PASSWORD")?;

-        Ok(Self {
-            client: ipfs_api_backend_hyper::IpfsClient::from_str(&client_url)?
-                .with_credentials(&username, &password),
-        })
+        let client = ipfs_api_backend_hyper::IpfsClient::from_str(&client_url)
+            .map_err(|e| anyhow::anyhow!("Failed to create IPFS client: {}", e))?
+            .with_credentials(&username, &password);

+        Ok(Self { client })
     }
 }
crates/dojo/world/src/metadata/metadata.rs (3)

12-19: Ohayo, sensei! Consider passing data by reference in compute_metadata_hash

Passing data by value might lead to unnecessary copies if T is a large type. It's more efficient to pass data as a reference.

Apply this diff to update the function signature:

- fn compute_metadata_hash<T>(data: T) -> u64
+ fn compute_metadata_hash<T>(data: &T) -> u64

And update the call site accordingly:

- let new_hash = compute_metadata_hash(self);
+ let new_hash = compute_metadata_hash(self);

50-58: Ohayo, sensei! Refactor duplicated code for uploading URIs

The code for uploading icon_uri and cover_uri is duplicated. Extracting a helper function can reduce repetition and improve maintainability.

For example, create a helper function:

async fn upload_uri(
    uri_option: &Option<Uri>,
    service: &mut impl MetadataService,
) -> Result<Option<Uri>> {
    if let Some(Uri::File(path)) = uri_option {
        let data = std::fs::read(path)?;
        let uploaded_uri = Uri::Ipfs(service.upload(data).await?);
        Ok(Some(uploaded_uri))
    } else {
        Ok(uri_option.clone())
    }
}

Then update the upload method:

- if let Some(Uri::File(icon)) = &self.icon_uri {
-     let icon_data = std::fs::read(icon)?;
-     meta.icon_uri = Some(Uri::Ipfs(service.upload(icon_data).await?));
- };
+ meta.icon_uri = upload_uri(&self.icon_uri, service).await?;

- if let Some(Uri::File(cover)) = &self.cover_uri {
-     let cover_data = std::fs::read(cover)?;
-     meta.cover_uri = Some(Uri::Ipfs(service.upload(cover_data).await?));
- };
+ meta.cover_uri = upload_uri(&self.cover_uri, service).await?;

68-77: Ohayo, sensei! Refactor duplicated code in ResourceMetadata

Similar to WorldMetadata, the code for uploading icon_uri can be refactored to use the helper function, reducing duplication.

Update the upload method in ResourceMetadata:

- if let Some(Uri::File(icon)) = &self.icon_uri {
-     let icon_data = std::fs::read(icon)?;
-     meta.icon_uri = Some(Uri::Ipfs(service.upload(icon_data).await?));
- };
+ meta.icon_uri = upload_uri(&self.icon_uri, service).await?;

Note: Ensure the helper function upload_uri is accessible to both implementations.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between e8a1bac and aeb376f.

📒 Files selected for processing (8)
  • bin/sozo/src/commands/migrate.rs (1 hunks)
  • crates/dojo/world/src/metadata/fake_metadata_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/ipfs_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test.rs (1 hunks)
  • crates/dojo/world/src/metadata/mod.rs (1 hunks)
  • crates/sozo/ops/src/migrate/mod.rs (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • bin/sozo/src/commands/migrate.rs
  • crates/dojo/world/src/metadata/mod.rs
🔇 Additional comments (5)
crates/dojo/world/src/metadata/metadata_service.rs (1)

5-9: The method signatures align well with the metadata upload requirements!

The async upload method will help solve the metadata upload issues after failed deployments (issue #2096). The test-only get method is appropriately gated behind #[cfg(test)].

Let's verify if there are any existing implementations of this trait:

✅ Verification successful

Ohayo sensei! The trait implementations are perfectly aligned with the requirements! ✨

Both IpfsMetadataService and FakeMetadataService correctly implement:

  • The async upload method with the required signature
  • The test-only get method (properly gated with #[cfg(test)] in IpfsMetadataService)

The implementations are consistent and ready to handle metadata upload functionality for issue #2096.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Find implementations of MetadataService trait
# Expected: Should find at least IpfsMetadataService and test implementations

# Search for struct/class implementations
ast-grep --pattern 'impl $_ for $_ where $$$'

# Search for direct implementations
rg "impl.*MetadataService.*for"

Length of output: 278


Script:

#!/bin/bash
# Let's check the implementations to verify they align with the trait definition
rg -B 5 -A 10 "impl MetadataService for" crates/dojo/world/src/metadata/ipfs_service.rs crates/dojo/world/src/metadata/fake_metadata_service.rs

Length of output: 2645

crates/sozo/ops/src/migrate/mod.rs (3)

23-23: LGTM! Well-organized imports

The new imports are properly organized and necessary for the metadata handling functionality.

Also applies to: 27-28, 32-32


114-168: LGTM! Well-structured metadata upload implementation

The implementation follows good practices:

  • Checks existing metadata hash before uploading
  • Uses batch transactions via multicall for efficiency
  • Properly handles different resource types (world, contracts, models, events)

170-197: LGTM! Efficient change detection implementation

The implementation efficiently handles metadata updates by:

  • Computing resource selectors
  • Checking for changes before uploading
  • Batching calls for efficiency
crates/dojo/world/src/metadata/ipfs_service.rs (1)

35-43: Ensure get Method is Only Used in Tests

The get method is conditionally compiled with #[cfg(test)], restricting it to test builds.

This is a good practice to prevent test-only code from being included in production builds.

crates/dojo/world/src/metadata/fake_metadata_service.rs Outdated Show resolved Hide resolved
crates/sozo/ops/src/migrate/mod.rs Show resolved Hide resolved
crates/dojo/world/src/metadata/ipfs_service.rs Outdated Show resolved Hide resolved
crates/dojo/world/src/metadata/ipfs_service.rs Outdated Show resolved Hide resolved
crates/dojo/world/src/metadata/metadata.rs Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Outside diff range and nitpick comments (10)
crates/dojo/world/src/metadata/ipfs_service.rs (2)

14-19: Ohayo, sensei! Consider enhancing Debug implementation

The current Debug implementation doesn't provide any useful information. Consider including the client URL for better debugging experience.

 impl std::fmt::Debug for IpfsMetadataService {
-    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
-        Ok(())
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+        write!(f, "IpfsMetadataService {{ client_url: {} }}", self.client.base_url())
     }
 }

39-47: Ohayo, sensei! Improve URI validation in get method

The get method should validate the URI format before processing.

     #[cfg(test)]
     async fn get(&self, uri: String) -> Result<Vec<u8>> {
+        if !uri.starts_with("ipfs://") {
+            return Err(anyhow::anyhow!("Invalid IPFS URI format: must start with ipfs://"));
+        }
         let res = self
             .client
             .cat(&uri.replace("ipfs://", ""))
             .map_ok(|chunk| chunk.to_vec())
             .try_concat()
-            .await?;
+            .await
+            .map_err(|e| anyhow::anyhow!("Failed to retrieve from IPFS: {}", e))?;
         Ok(res)
     }
bin/sozo/src/commands/migrate.rs (1)

84-90: Excellent implementation sensei, with a minor suggestion

The metadata service integration looks solid and addresses the core objectives of uploading metadata after failed deployments. The error handling is good but could be more descriptive.

Consider enhancing the error context:

 let mut metadata_service =
-    IpfsMetadataService::new(IPFS_CLIENT_URL, IPFS_USERNAME, IPFS_PASSWORD)?;
+    IpfsMetadataService::new(IPFS_CLIENT_URL, IPFS_USERNAME, IPFS_PASSWORD)
+        .context("Failed to initialize IPFS metadata service")?;

 migration
     .upload_metadata(&mut spinner, &mut metadata_service)
     .await
-    .context("Metadata upload failed.")?;
+    .context("Failed to upload world metadata to IPFS")?;
crates/dojo/world/src/metadata/metadata_test.rs (3)

16-21: Ohayo! Consider making test file paths more robust, sensei.

The hardcoded path ./src/metadata/metadata_test_data/ might break if tests are run from different working directories.

Consider using environment variables or relative paths from the crate root:

-    fs::canonicalize(
-        PathBuf::from_str(&format!("./src/metadata/metadata_test_data/{}", filename)).unwrap(),
-    )
+    let test_data_dir = env!("CARGO_MANIFEST_DIR").to_owned() + "/src/metadata/metadata_test_data";
+    fs::canonicalize(PathBuf::from_str(&format!("{}/{}", test_data_dir, filename)).unwrap())

58-132: Enhance test coverage with error cases, sensei.

While the happy path is well tested, consider adding test cases for:

  1. Invalid file paths
  2. Malformed metadata
  3. Service failures

Also, consider extracting magic values:

+ const HASH_INCREMENT: Felt = Felt::ONE;
- world_metadata.upload_if_changed(&mut metadata_service, current_hash + Felt::ONE).await
+ world_metadata.upload_if_changed(&mut metadata_service, current_hash + HASH_INCREMENT).await

1-189: Excellent alignment with PR objectives, sensei! 🎯

The tests thoroughly validate the core requirement of uploading metadata only when changes are detected:

  • Hash-based change detection
  • Proper re-upload behavior
  • Comprehensive content verification

Consider adding integration tests that simulate the full deployment workflow, including failure scenarios mentioned in issue #2096.

crates/sozo/ops/src/migrate/mod.rs (2)

114-168: Consider enhancing error handling and progress tracking

Ohayo sensei! The metadata upload implementation looks solid, but could benefit from some improvements:

  1. Consider adding retry logic for metadata service failures
  2. Add progress tracking for large uploads (e.g., X/Y resources uploaded)
 pub async fn upload_metadata(
     &self,
     ui: &mut MigrationUi,
     service: &mut impl MetadataService,
 ) -> anyhow::Result<()> {
     ui.update_text("Uploading metadata...");
+    let total_resources = self.diff.resources.len();
+    let mut uploaded = 0;
 
     let mut invoker = Invoker::new(&self.world.account, self.txn_config);
 
     // world
     let current_hash =
         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(service, current_hash).await?;
+    let res = new_metadata
+        .upload_if_changed(service, current_hash)
+        .await
+        .map_err(|e| anyhow!("Failed to upload world metadata: {}", e))?;
+    uploaded += 1;
+    ui.update_text(&format!("Uploaded {}/{} resources...", uploaded, total_resources));

123-154: Consider architectural optimizations for metadata handling

Ohayo sensei! The current implementation is solid, but here are some architectural suggestions to improve performance and scalability:

  1. Consider implementing parallel uploads for metadata using futures::stream::StreamExt
  2. Add caching layer for frequently accessed metadata to reduce IPFS calls
  3. Implement batch uploads for related resources

Would you like me to provide an example implementation of these optimizations?

crates/dojo/world/src/metadata/metadata_storage.rs (2)

63-64: Optimize metadata serialization to improve efficiency

Ohayo, sensei! Instead of using json!(meta).to_string() and then converting it to bytes, you can directly serialize meta to a byte vector using serde_json::to_vec(&meta)?. This approach reduces unnecessary intermediate allocations and improves performance.


75-76: Optimize metadata serialization to improve efficiency

Ohayo, sensei! Similar to the previous suggestion, consider using serde_json::to_vec(&meta)? to serialize ResourceMetadata directly to a byte vector. This enhances efficiency by avoiding intermediate string conversions.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between aeb376f and 7c34cc3.

📒 Files selected for processing (9)
  • bin/sozo/src/commands/migrate.rs (3 hunks)
  • crates/dojo/world/src/metadata/fake_metadata_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/ipfs_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_storage.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test.rs (1 hunks)
  • crates/dojo/world/src/metadata/mod.rs (1 hunks)
  • crates/sozo/ops/src/migrate/mod.rs (2 hunks)
  • crates/sozo/ops/src/tests/migration.rs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • crates/dojo/world/src/metadata/fake_metadata_service.rs
  • crates/dojo/world/src/metadata/metadata_service.rs
  • crates/dojo/world/src/metadata/mod.rs
  • crates/sozo/ops/src/tests/migration.rs
🔇 Additional comments (7)
crates/dojo/world/src/metadata/ipfs_service.rs (1)

1-12: Ohayo, sensei! Clean and well-organized structure!

The imports are minimal and well-organized, and the struct definition follows good practices with clear separation of concerns.

bin/sozo/src/commands/migrate.rs (2)

6-6: LGTM! Clean import for metadata service

The import is well-placed and properly scoped under the dojo_world module.


87-90: Verify no duplicate metadata upload implementations

Let's ensure this is the only place handling metadata uploads to avoid potential conflicts.

✅ Verification successful

No duplicate metadata upload implementations found, sensei!

The implementation is clean and well-organized:

  • The main upload_metadata method in migrate/mod.rs is the core implementation
  • Other occurrences are either:
    • The interface definition in metadata/mod.rs
    • Test implementations in tests/migration.rs
    • The actual service implementation in ipfs_service.rs

The TODO comment in build.rs is unrelated to the upload functionality and just refers to migration process timing.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for other metadata upload implementations
echo "Searching for other metadata upload implementations..."
rg -l "upload_metadata|IpfsMetadataService" --type rust

# Search for any TODO comments related to metadata
echo "Checking for metadata-related TODOs..."
rg -i "todo.*metadata" --type rust

Length of output: 602


Script:

#!/bin/bash
# Let's check the actual implementation details of metadata uploads
echo "Checking metadata upload implementations in migration module..."
rg "upload_metadata" -A 5 -B 5 crates/sozo/ops/src/migrate/mod.rs

echo "\nChecking metadata upload implementations in IPFS service..."
rg "upload_metadata" -A 5 -B 5 crates/dojo/world/src/metadata/ipfs_service.rs

echo "\nChecking test implementations..."
rg "upload_metadata" -A 5 -B 5 crates/sozo/ops/src/tests/migration.rs

Length of output: 2659

crates/dojo/world/src/metadata/metadata_test.rs (2)

23-56: Well-structured test helpers, sensei! 🎉

The helper functions are well-designed:

  • Clear separation of concerns
  • Reusable assertions
  • Consistent test data generation

134-189: Clean and thorough test implementation, sensei! ✨

The test provides good coverage of the resource metadata functionality with:

  • Clear state transitions
  • Comprehensive assertions
  • Proper error handling checks
crates/sozo/ops/src/migrate/mod.rs (1)

178-179: ⚠️ Potential issue

Add error handling for selector computation

Ohayo sensei! This issue was previously identified and still needs to be addressed.

Apply this diff as suggested in the past review:

-    let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag);
+    let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag)
+        .map_err(|e| anyhow!("Failed to compute selector for tag {}: {}", item.tag, e))?;
crates/dojo/world/src/metadata/metadata_storage.rs (1)

43-44: Verify correct conversion from u64 hash to Felt

Ohayo, sensei! When converting the u64 hash to a Felt using Felt::from_raw([0, 0, 0, new_hash]), only the lower 64 bits are used. If the full 251-bit range of Felt is required for collision resistance, consider using a hash function that produces a larger output, such as Sha256, and appropriately mapping it into a Felt.

crates/dojo/world/src/metadata/ipfs_service.rs Outdated Show resolved Hide resolved
crates/dojo/world/src/metadata/ipfs_service.rs Outdated Show resolved Hide resolved
Comment on lines 23 to 26
// 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";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo! Critical security concern: Remove hardcoded IPFS credentials

The hardcoded IPFS credentials pose a significant security risk. These should be moved to environment variables immediately.

Apply this diff to implement environment variables:

-// 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";
+use std::env;
+
+fn get_ipfs_config() -> Result<(String, String, String)> {
+    Ok((
+        env::var("IPFS_CLIENT_URL").context("IPFS_CLIENT_URL not set")?,
+        env::var("IPFS_USERNAME").context("IPFS_USERNAME not set")?,
+        env::var("IPFS_PASSWORD").context("IPFS_PASSWORD not set")?,
+    ))
+}

Committable suggestion skipped: line range outside the PR's diff.

crates/dojo/world/src/metadata/metadata_storage.rs Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Outside diff range and nitpick comments (10)
crates/dojo/world/src/metadata/ipfs_service.rs (2)

16-21: Ohayo, sensei! Consider enhancing Debug implementation

The current Debug implementation, while satisfying clippy, could be more helpful for debugging purposes. Consider including the client URL in the debug output.

 impl std::fmt::Debug for IpfsMetadataService {
-    fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
-        Ok(())
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+        write!(f, "IpfsMetadataService {{ client_url: {} }}", self.client.base_url())
     }
 }

51-59: Ohayo, sensei! Add validation for empty data in upload method

Consider adding validation to prevent uploading empty data to IPFS.

 async fn upload(&mut self, data: Vec<u8>) -> Result<String> {
+    if data.is_empty() {
+        return Err(anyhow::anyhow!("Cannot upload empty data to IPFS"));
+    }
     let reader = Cursor::new(data);
     let response = self
         .client
crates/dojo/world/src/metadata/metadata_storage.rs (3)

39-47: Consider using streaming for large file uploads

Ohayo, sensei! While the current implementation works well for small files, loading the entire file into memory with std::fs::read could be problematic for large files. Consider implementing streaming upload support for handling large files efficiently.

 async fn upload_uri(uri: &Option<Uri>, service: &mut impl MetadataService) -> Result<Option<Uri>> {
     if let Some(Uri::File(path)) = uri {
-        let data = std::fs::read(path)?;
-        let uploaded_uri = Uri::Ipfs(service.upload(data).await?);
+        let file = tokio::fs::File::open(path).await?;
+        let uploaded_uri = Uri::Ipfs(service.upload_stream(file).await?);
         Ok(Some(uploaded_uri))
     } else {
         Ok(uri.clone())
     }
 }

70-87: Simplify hash comparison logic

Ohayo, sensei! The current implementation converts the hash to a Felt type for comparison, which adds unnecessary complexity. Consider using the native u64 hash value directly for comparison, as it would be more efficient and clearer.

     async fn upload_if_changed(
         &self,
         service: &mut impl MetadataService,
-        current_hash: Felt,
+        current_hash: u64,
     ) -> Result<Option<(String, u64)>> {
         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)
     }

1-113: Consider adding monitoring capabilities

Ohayo, sensei! To better understand the system's behavior in production, consider adding monitoring capabilities:

  • Track metadata upload latency
  • Monitor hash computation time
  • Count upload successes and failures
  • Track file sizes being processed

This would help identify performance bottlenecks and potential issues early.

crates/sozo/ops/src/tests/migration.rs (3)

43-43: The migration function changes look solid, sensei!

The addition of the with_metadata parameter and conditional upload block implements the core functionality needed to address issues #2096 and #1907. The error handling is descriptive and helpful.

Consider adding a doc comment for the with_metadata parameter to clarify its purpose:

/// Migrates the spawn-and-move project from the local environment.
+ /// 
+ /// # Arguments
+ /// * `sequencer` - The runner context for the test
+ /// * `with_metadata` - When true, uploads world/models/contracts metadata after migration

Also applies to: 64-70


77-77: Test updates maintain existing behavior correctly!

The existing tests have been properly updated to pass false for the with_metadata parameter, ensuring they maintain their original behavior.

Consider adding negative test cases to verify that metadata is not uploaded when with_metadata is false:

assert!(is_not_set(world_diff.world_info.metadata_hash));

Also applies to: 86-89


91-113: Excellent helper function implementation, sensei!

The check_resources helper is well-structured and provides good reusability for metadata verification.

Consider making the error message more specific by including the expected and actual hash values:

- assert!(checker(resource.metadata_hash()), "Bad resource hash: {}", resource.name());
+ assert!(checker(resource.metadata_hash()), "Invalid metadata hash for resource {}: expected {} to be {}", 
+   resource.name(), 
+   resource.metadata_hash(),
+   if checker == is_set { "non-zero" } else { "zero" }
+ );
crates/sozo/ops/src/migrate/mod.rs (2)

114-168: Enhance error message in multicall error handling

Ohayo sensei! The implementation looks solid with good practices for metadata handling and UI feedback. However, the error message in the multicall error handling could be more descriptive.

Consider this enhancement:

-            invoker.multicall().await.map_err(|e| anyhow!(e.to_string()))?;
+            invoker.multicall().await.map_err(|e| anyhow!("Failed to upload metadata via multicall: {}", e))?;

170-197: Consider using early returns for cleaner flow

Ohayo sensei! The implementation is solid, but we can make it more idiomatic with early returns.

Consider this refactor:

     async fn upload_metadata_from_resource_config(
         &self,
         service: &mut impl MetadataService,
         config: &[ResourceConfig],
     ) -> anyhow::Result<Vec<Call>> {
         let mut calls = vec![];
 
         for item in config {
             let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag);
 
             let current_hash =
                 self.diff.resources.get(&selector).map_or(Felt::ZERO, |r| r.metadata_hash());
 
             let new_metadata = metadata_config::ResourceMetadata::from(item.clone());
 
-            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 {
-                    resource_id: selector,
-                    metadata_uri: ByteArray::from_string(&new_uri)?,
-                    metadata_hash: new_hash,
-                }));
-            }
+            let Some((new_uri, new_hash)) = new_metadata.upload_if_changed(service, current_hash).await? else {
+                continue;
+            };
+            
+            calls.push(self.world.set_metadata_getcall(&ResourceMetadata {
+                resource_id: selector,
+                metadata_uri: ByteArray::from_string(&new_uri)?,
+                metadata_hash: new_hash,
+            }));
         }
 
         Ok(calls)
     }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 7c34cc3 and 1da4708.

📒 Files selected for processing (9)
  • bin/sozo/src/commands/migrate.rs (3 hunks)
  • crates/dojo/world/src/metadata/fake_metadata_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/ipfs_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_storage.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test.rs (1 hunks)
  • crates/dojo/world/src/metadata/mod.rs (1 hunks)
  • crates/sozo/ops/src/migrate/mod.rs (2 hunks)
  • crates/sozo/ops/src/tests/migration.rs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • bin/sozo/src/commands/migrate.rs
  • crates/dojo/world/src/metadata/fake_metadata_service.rs
  • crates/dojo/world/src/metadata/metadata_service.rs
  • crates/dojo/world/src/metadata/metadata_test.rs
  • crates/dojo/world/src/metadata/mod.rs
🔇 Additional comments (5)
crates/dojo/world/src/metadata/ipfs_service.rs (2)

1-14: Ohayo, sensei! Clean and well-documented struct definition

The imports are appropriate, and the struct documentation clearly explains its purpose. The minimal design with a single client field follows good practices.


23-47: Ohayo, sensei! Robust implementation with proper validation

The new method implementation is excellent with:

  • Comprehensive input validation
  • Clear error messages
  • Well-documented parameters and return values
crates/sozo/ops/src/tests/migration.rs (2)

8-8: Ohayo! The new imports look good, sensei!

The added imports for ResourceConfig, FakeMetadataService, and Felt align perfectly with the PR's metadata handling objectives.

Also applies to: 11-11, 17-17


115-152: Comprehensive metadata upload test implementation!

The test thoroughly verifies:

  1. Initial state with no metadata
  2. Successful metadata upload after migration
  3. Proper updates in the ResourceMetadata model

The clear separation between setup, action, and verification makes the test easy to understand.

crates/sozo/ops/src/migrate/mod.rs (1)

23-23: LGTM! Clean import additions

The new imports are well-organized and properly scoped for the metadata functionality.

Also applies to: 27-28, 32-32

crates/dojo/world/src/metadata/ipfs_service.rs Outdated Show resolved Hide resolved
crates/dojo/world/src/metadata/metadata_storage.rs Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Outside diff range and nitpick comments (13)
scripts/rust_fmt.sh (1)

3-3: Consider adding documentation for the script usage

Ohayo! It would be helpful to add a comment block explaining:

  • The script's purpose
  • Available options (especially the new --fix flag)
  • Example usage

Here's a suggested addition:

#!/bin/bash

+# Rust code formatting script
+# Usage:
+#   ./rust_fmt.sh           # Check formatting without making changes
+#   ./rust_fmt.sh --fix     # Apply formatting fixes
+#   
+# Additional cargo fmt arguments can be passed after the initial options
+
 option="--check"
examples/spawn-and-move/dojo_dev.toml (1)

6-17: Ohayo! Consider enhancing model descriptions, sensei!

While the model definitions are clear, they could benefit from more detailed descriptions including:

  • Expected fields/properties
  • Relationships with other models
  • Usage examples
crates/dojo/world/src/config/environment.rs (2)

13-15: Ohayo! Consider enhancing security for IPFS credentials, sensei!

While the implementation follows the existing pattern, storing sensitive credentials (username/password) as plain text in a struct that derives Debug could potentially expose them in logs or error messages.

Consider:

  1. Creating a dedicated type for credentials that implements custom Debug
  2. Using a secure string type that zeros memory on drop

Here's a suggested approach:

use secrecy::SecretString;

#[derive(Default, Deserialize, Clone)]
pub struct IpfsCredentials {
    username: Option<String>,
    password: Option<SecretString>,
}

impl Debug for IpfsCredentials {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("IpfsCredentials")
            .field("username", &self.username)
            .field("password", &"[REDACTED]")
            .finish()
    }
}

Line range hint 13-59: Ohayo! Consider adding configuration validation, sensei!

Since this is part of the metadata upload enhancement, it might be helpful to:

  1. Add a method to validate IPFS configuration completeness (all or none of the IPFS fields should be set)
  2. Document the IPFS configuration requirements in the struct documentation

Here's a suggested validation method:

impl Environment {
    pub fn validate_ipfs_config(&self) -> Result<(), &'static str> {
        match (self.ipfs_url(), self.ipfs_username(), self.ipfs_password()) {
            (None, None, None) => Ok(()), // No IPFS config is valid
            (Some(_), Some(_), Some(_)) => Ok(()), // Complete IPFS config is valid
            _ => Err("Incomplete IPFS configuration: all or none of the IPFS fields must be set")
        }
    }
}
crates/dojo/world/src/metadata/metadata_storage.rs (1)

91-116: Reduce code duplication in trait implementations

Ohayo, sensei! The upload implementations for WorldMetadata and ResourceMetadata share significant code. Consider extracting the common logic into a helper function.

+async fn upload_metadata<T: serde::Serialize>(
+    meta: &mut T,
+    icon_uri: &Option<Uri>,
+    service: &mut impl MetadataService
+) -> Result<String> {
+    if let Some(new_uri) = upload_uri(icon_uri, service)
+        .await
+        .context("Failed to upload icon URI")? {
+        // Assuming there's a way to set icon_uri on T
+        meta.set_icon_uri(new_uri);
+    }
+    
+    let serialized = json!(meta).to_string();
+    service.upload(serialized.as_bytes().to_vec())
+        .await
+        .context("Failed to upload metadata")
+}

 impl MetadataStorage for WorldMetadata {
     async fn upload(&self, service: &mut impl MetadataService) -> Result<String> {
         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")
+        upload_metadata(&mut meta, &self.icon_uri, service).await
     }
 }
bin/sozo/src/commands/migrate.rs (1)

84-105: Enhance error handling and credential validation

Ohayo sensei! While the implementation works, we can make it more robust:

  1. Replace multiple unwrap() calls with safer alternatives
  2. Consolidate credential validation
  3. Provide more specific error messages

Consider this improvement:

-            let ipfs_url = ipfs.url(profile_config.env.as_ref());
-            let ipfs_username = ipfs.username(profile_config.env.as_ref());
-            let ipfs_password = ipfs.password(profile_config.env.as_ref());
-
-            let mut metadata_upload_text = String::new();
-
-            if ipfs_url.is_some() && ipfs_username.is_some() && ipfs_password.is_some() {
-                let mut metadata_service = IpfsMetadataService::new(
-                    &ipfs_url.unwrap(),
-                    &ipfs_username.unwrap(),
-                    &ipfs_password.unwrap(),
-                )?;
+            let mut metadata_upload_text = match (
+                ipfs.url(profile_config.env.as_ref()),
+                ipfs.username(profile_config.env.as_ref()),
+                ipfs.password(profile_config.env.as_ref()),
+            ) {
+                (Some(url), Some(username), Some(password)) => {
+                    let mut metadata_service = IpfsMetadataService::new(&url, &username, &password)
+                        .context("Failed to initialize IPFS metadata service")?;
+                    
+                    migration
+                        .upload_metadata(&mut spinner, &mut metadata_service)
+                        .await
+                        .context("Failed to upload metadata to IPFS")?;
+                    String::new()
+                }
+                _ => "\nMetadata: No IPFS credentials found => metadata upload ignored".to_string()
+            };
-                migration
-                    .upload_metadata(&mut spinner, &mut metadata_service)
-                    .await
-                    .context("Metadata upload failed.")?;
-            } else {
-                metadata_upload_text = "\nMetadata: No IPFS credentials has been found => \
-                                        metadata upload has been ignored."
-                    .to_string();
-            };
crates/sozo/ops/src/tests/migration.rs (1)

91-113: Consider adding documentation for the helper function, sensei!

The check_resources helper is well-structured with good assertions and error messages. However, it would benefit from documentation explaining:

  • The purpose of the checker callback
  • The expected format of resources
  • The meaning of expected_count

Consider adding a doc comment like this:

+/// Validates resource metadata in a WorldDiff against expected criteria
+/// 
+/// # Arguments
+/// * `diff` - The WorldDiff containing resources to validate
+/// * `resources` - Optional vector of ResourceConfig to check
+/// * `expected_count` - Expected number of resources
+/// * `checker` - Callback to validate the metadata hash of each resource
 fn check_resources(
bin/sozo/src/commands/options/ipfs.rs (3)

9-27: Ohayo! Consider enhancing security for sensitive fields.

While the implementation looks solid, sensei, we should consider adding a security note in the help text for the password field to warn users about command-line visibility.

     #[arg(long, env = IPFS_PASSWORD_ENV_VAR)]
     #[arg(value_name = "PASSWORD")]
-    #[arg(help = "The IPFS password.")]
+    #[arg(help = "The IPFS password. Note: Prefer using environment variables for passwords to avoid shell history exposure.")]
     #[arg(global = true)]
     pub ipfs_password: Option<String>,

29-74: Consider refactoring to reduce duplication and enhance security.

The implementation looks good but has two areas for improvement:

  1. The three methods follow the same pattern and could be refactored to reduce duplication
  2. The password tracing at line 64 might expose sensitive information in logs

Consider implementing a generic method to handle the common pattern:

impl IpfsOptions {
    fn get_value<T: Clone>(
        &self,
        cmd_value: &Option<T>,
        env_value: Option<&Environment> -> Option<String>,
        field_name: &str,
    ) -> Option<String> {
        trace!("Retrieving {} for IpfsOptions.", field_name);

        if let Some(value) = cmd_value {
            trace!("Using IPFS {} from command line.", field_name);
            Some(value.to_string())
        } else if let Some(value) = env_value {
            trace!("Using IPFS {} from environment metadata.", field_name);
            Some(value)
        } else {
            trace!("No default IPFS {}.", field_name);
            None
        }
    }
}

Also, consider masking the password in trace logs:

-            trace!(?password, "Using IPFS password from command line.");
+            trace!("Using IPFS password from command line. [REDACTED]");

76-170: Excellent test coverage, sensei! Consider adding edge cases.

The test suite is comprehensive and well-structured. To make it even more robust, consider adding tests for:

  1. Invalid URL formats
  2. Empty string values
  3. Unicode characters in username/password

Example test case:

#[test]
fn handles_invalid_url_format() {
    let cmd = Command::parse_from([
        "sozo",
        "--ipfs-url",
        "not_a_valid_url",
    ]);
    assert!(cmd.options.url(None).is_none());
}
crates/sozo/ops/src/migrate/mod.rs (2)

114-168: Consider adding progress tracking and retry mechanism for robustness

Ohayo sensei! The metadata upload implementation looks solid, but could benefit from these enhancements:

  1. Progress tracking for large uploads (e.g., number of successful/failed uploads)
  2. Retry mechanism for failed uploads to handle temporary network issues
 pub async fn upload_metadata(
     &self,
     ui: &mut MigrationUi,
     service: &mut impl MetadataService,
 ) -> anyhow::Result<()> {
     ui.update_text("Uploading metadata...");
+    let mut success_count = 0;
+    let mut retry_count = 0;
+    const MAX_RETRIES: u32 = 3;
 
     let mut invoker = Invoker::new(&self.world.account, self.txn_config);
 
     // world
     let current_hash =
         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(service, current_hash).await?;
+    let res = retry_with_backoff(MAX_RETRIES, || {
+        retry_count += 1;
+        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,6 +137,7 @@
             metadata_uri: ByteArray::from_string(&new_uri)?,
             metadata_hash: new_hash,
         }));
+        success_count += 1;
     }

170-197: Consider batch processing optimization for resource configs

Ohayo sensei! The current implementation processes each resource config sequentially. For large configurations, consider implementing batch processing to improve performance.

 async fn upload_metadata_from_resource_config(
     &self,
     service: &mut impl MetadataService,
     config: &[ResourceConfig],
 ) -> anyhow::Result<Vec<Call>> {
     let mut calls = vec![];
+    const BATCH_SIZE: usize = 50;
 
-    for item in config {
+    for batch in config.chunks(BATCH_SIZE) {
+        let futures: Vec<_> = batch.iter().map(|item| async {
+            let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag)
+                .map_err(|e| anyhow!("Failed to compute selector for tag {}: {}", item.tag, e))?;
 
-        let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag);
+            let current_hash =
+                self.diff.resources.get(&selector).map_or(Felt::ZERO, |r| r.metadata_hash());
 
-        let current_hash =
-            self.diff.resources.get(&selector).map_or(Felt::ZERO, |r| r.metadata_hash());
+            let new_metadata = metadata_config::ResourceMetadata::from(item.clone());
+            let res = new_metadata.upload_if_changed(service, current_hash).await?;
 
-        let new_metadata = metadata_config::ResourceMetadata::from(item.clone());
-
-        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 {
+            Ok::<Option<Call>, anyhow::Error>(if let Some((new_uri, new_hash)) = res {
+                Some(self.world.set_metadata_getcall(&ResourceMetadata {
                 resource_id: selector,
                 metadata_uri: ByteArray::from_string(&new_uri)?,
                 metadata_hash: new_hash,
-            }));
+                }))
+            } else {
+                None
+            })
+        }).collect();
+
+        let results = futures::future::join_all(futures).await;
+        for result in results {
+            if let Some(call) = result? {
+                calls.push(call);
+            }
         }
     }
 
     Ok(calls)
 }
examples/spawn-and-move/manifest_dev.json (1)

95-98: Ohayo! Excellent addition of metadata hash tracking.

The new metadata_hash field in ResourceMetadata and corresponding hash field in MetadataUpdate event directly support the PR's objective of improved metadata handling. This will enable detecting when metadata content has actually changed, ensuring uploads happen only when necessary.

This change provides a robust foundation for:

  • Detecting actual changes in metadata content
  • Preventing unnecessary uploads
  • Tracking metadata versions

Also applies to: 1009-1013

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 1da4708 and 9ffe3e7.

⛔ Files ignored due to path filters (2)
  • spawn-and-move-db.tar.gz is excluded by !**/*.gz
  • types-test-db.tar.gz is excluded by !**/*.gz
📒 Files selected for processing (17)
  • bin/sozo/src/commands/dev.rs (2 hunks)
  • bin/sozo/src/commands/migrate.rs (6 hunks)
  • bin/sozo/src/commands/options/ipfs.rs (1 hunks)
  • bin/sozo/src/commands/options/mod.rs (1 hunks)
  • crates/dojo/utils/src/env.rs (1 hunks)
  • crates/dojo/world/src/config/environment.rs (2 hunks)
  • crates/dojo/world/src/metadata/fake_metadata_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/ipfs_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_service.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_storage.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test.rs (1 hunks)
  • crates/dojo/world/src/metadata/mod.rs (1 hunks)
  • crates/sozo/ops/src/migrate/mod.rs (2 hunks)
  • crates/sozo/ops/src/tests/migration.rs (4 hunks)
  • examples/spawn-and-move/dojo_dev.toml (2 hunks)
  • examples/spawn-and-move/manifest_dev.json (8 hunks)
  • scripts/rust_fmt.sh (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • crates/dojo/world/src/metadata/fake_metadata_service.rs
  • crates/dojo/world/src/metadata/ipfs_service.rs
  • crates/dojo/world/src/metadata/metadata_service.rs
  • crates/dojo/world/src/metadata/metadata_test.rs
  • crates/dojo/world/src/metadata/mod.rs
🔇 Additional comments (20)
bin/sozo/src/commands/options/mod.rs (1)

2-2: Ohayo! The IPFS module declaration looks good, sensei!

The addition of the IPFS module aligns well with the PR objectives for improving metadata handling. The module is correctly declared and properly placed in alphabetical order.

Let's verify the IPFS module implementation:

✅ Verification successful

Ohayo! The IPFS module implementation looks solid, sensei!

The IPFS module has been properly implemented with:

  • A well-structured IpfsOptions struct with URL, username, and password fields
  • Comprehensive environment variable support via clap::Args
  • Clear helper methods for retrieving configuration from both CLI and environment
  • Thorough test coverage including all configuration scenarios
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the IPFS module structure and integration
# Check for the IPFS module file and its contents
fd "ipfs.rs" --type f --exec cat {} \;

# Look for any IPFS-related structs and their usage
ast-grep --pattern 'struct $name {
  $$$
}'

Length of output: 6289

scripts/rust_fmt.sh (1)

4-11: Ohayo! Clean and effective implementation, sensei!

The changes introduce a flexible way to toggle between check and fix modes while maintaining backward compatibility. The implementation follows shell scripting best practices with proper argument handling.

crates/dojo/utils/src/env.rs (1)

7-9: Ohayo! The new IPFS environment variables look well-structured, sensei!

The naming convention is consistent with existing patterns, and these additions align perfectly with the metadata upload improvements.

examples/spawn-and-move/dojo_dev.toml (2)

18-25: LGTM! Event and contract definitions are well-structured.

The event and contract definitions align well with the models and follow consistent naming conventions.


34-34: Verify world address update impact

The world address has been updated. Please ensure this change is intentional and won't break existing deployments.

✅ Verification successful

World address change is isolated to spawn-and-move example

Ohayo sensei! The world address change in examples/spawn-and-move/dojo_dev.toml is safe. This address is only referenced in the spawn-and-move example's configuration files (dojo_dev.toml and manifest_dev.json). Each example and component in the codebase maintains its own unique world address, so this change won't affect other deployments.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any other files referencing the old world address
rg "0x70058e3886cb7411e8a77db90ee3dd453ac16b763b30bd99b3c8440fe42056e"

Length of output: 70


Script:

#!/bin/bash
# Let's search for any references to world addresses in the codebase
rg -i "world_address.*=.*0x[a-fA-F0-9]+" --type toml

# Also search for any references to the new world address
rg "0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea"

# Check if there are any other TOML config files
fd -e toml

Length of output: 3984

crates/dojo/world/src/config/environment.rs (1)

49-59: LGTM! Clean and consistent implementation, sensei!

The new accessor methods follow the established pattern perfectly, using as_deref() for safe string slice conversion. The implementation is clean and maintains consistency with the existing codebase.

bin/sozo/src/commands/migrate.rs (3)

6-6: LGTM! Clean import additions

Ohayo! The new imports for IPFS functionality are well-organized and properly scoped.

Also applies to: 18-18


37-39: LGTM! Clean struct extension

The IpfsOptions integration is properly flattened into the command structure.


113-127: LGTM! Clear and informative output messages

The status messages clearly indicate both the migration result and metadata upload status.

crates/sozo/ops/src/tests/migration.rs (4)

8-8: Ohayo! The new imports look good, sensei!

The added imports for ResourceConfig, FakeMetadataService, and Felt are well-organized and necessary for the new metadata functionality.

Also applies to: 11-11, 17-17


43-43: Clean implementation of optional metadata upload, sensei!

The addition of the with_metadata parameter and conditional metadata upload block aligns perfectly with the PR objectives. The error handling is descriptive and the separation of concerns is well-maintained.

Also applies to: 64-71


77-77: Test modifications maintain backward compatibility!

The updates to migrate_from_local and migrate_no_change properly handle the new with_metadata parameter while preserving their original test objectives.

Also applies to: 86-86


115-152: Excellent test coverage for metadata upload functionality!

The test is well-structured with:

  • Clear verification of initial state (no metadata)
  • Proper execution of migration with metadata
  • Comprehensive validation of metadata updates
  • Good comments explaining the test flow and scope

The note about IPFS upload testing being handled elsewhere shows good awareness of test boundaries.

bin/sozo/src/commands/dev.rs (2)

19-19: Ohayo! LGTM - Import statement is well-placed.

The import aligns with the PR's objective of enhancing metadata handling capabilities.


91-91: Verify the intentional disabling of IPFS metadata upload in dev mode

Ohayo sensei! I notice we're setting IpfsOptions::default() with a comment indicating IPFS metadata upload isn't needed here. However, given the PR's objective to "upload world/models/contracts metadata only if changed", should we consider enabling IPFS uploads in dev mode to catch metadata-related issues early?

Let's check how metadata uploads are handled in other modes:

✅ Verification successful

Let's analyze how MigrateArgs is structured and how metadata/IPFS handling is implemented across the codebase to better understand if this default behavior is correct.


Disabling IPFS metadata upload in dev mode is intentional and correct

Ohayo sensei! After analyzing the codebase, I can confirm that disabling IPFS metadata upload in dev mode is the correct behavior because:

  1. The dev mode is a special mode that runs migrations for local development and testing
  2. The metadata upload is properly handled in the regular migrate command, where:
    • It checks for IPFS credentials (url, username, password)
    • Only uploads if credentials are available
    • Provides feedback about skipped uploads when credentials are missing

The comment "no need for IPFS metadata upload here" is accurate and aligns with the development workflow where metadata uploads are unnecessary for local testing.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check how IPFS options are handled in other commands
# Expected: Find differences in IPFS handling between dev and other modes

# Search for IpfsOptions usage patterns
rg -A 5 "IpfsOptions::" bin/sozo/src/commands/

# Search for metadata upload logic
ast-grep --pattern 'upload_metadata($$$)' 

Length of output: 426


Script:

#!/bin/bash
# Let's check the MigrateArgs struct definition and its usage
ast-grep --pattern 'struct MigrateArgs {
  $$$
}'

# Check for any IPFS-related configuration or handling
rg -B 2 -A 5 "ipfs" bin/sozo/src/

# Look for metadata upload implementations
rg -B 2 -A 5 "metadata" bin/sozo/src/

Length of output: 58881

bin/sozo/src/commands/options/ipfs.rs (1)

1-170: Verify integration with metadata upload functionality.

The implementation looks solid for handling IPFS configuration. Let's verify how it integrates with the metadata upload functionality mentioned in the PR objectives.

✅ Verification successful

Ohayo sensei! After reviewing the implementation, I can now provide a definitive response:

IPFS integration is properly implemented ✨

The code in migrate.rs shows that the IPFS options are correctly used for metadata upload:

  • IPFS credentials are properly checked before attempting upload
  • The IpfsMetadataService is initialized with the provided credentials
  • Graceful fallback with informative message when IPFS credentials are missing
  • The implementation follows the command pattern and integrates well with the migration workflow
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for metadata upload implementation that uses these IPFS options
rg -l "IpfsOptions" | xargs rg "metadata.*upload"

# Look for any migration-related code that uses these options
ast-grep --pattern 'fn $_($$_: &IpfsOptions, $$$) {
  $$$
}'

Length of output: 800


Script:

#!/bin/bash
# Let's check the full implementation of migrate command to understand the IPFS integration
rg -A 20 "pub fn migrate" bin/sozo/src/commands/migrate.rs

# Also check if there are any IPFS-related utility functions
rg -l "ipfs" | xargs rg "fn.*ipfs"

Length of output: 713


Script:

#!/bin/bash
# Let's check the full migrate command implementation to see how IPFS options are used
cat bin/sozo/src/commands/migrate.rs

# Also check the metadata service implementation
rg -l "MetadataService" | xargs rg -A 10 "struct MetadataService"

Length of output: 5365

crates/sozo/ops/src/migrate/mod.rs (1)

178-179: Add error handling for selector computation

This issue was previously identified. The selector computation needs proper error handling to prevent silent failures.

examples/spawn-and-move/manifest_dev.json (3)

3-4: LGTM! World configuration updates look valid.

The new class hash and address values are properly formatted and maintain the expected structure.


1255-1256: Contract deployment updates look valid, sensei!

The updated contract addresses and class hashes maintain proper formatting and structure.

Also applies to: 1596-1596, 1790-1790, 1966-1966


2190-2190: Event configuration updates are properly structured.

The new class hashes for the events maintain the expected format.

Also applies to: 2196-2196

crates/dojo/utils/src/env.rs Show resolved Hide resolved
examples/spawn-and-move/dojo_dev.toml Show resolved Hide resolved
bin/sozo/src/commands/dev.rs Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (1)
bin/sozo/tests/test_data/policies.json (1)

47-52: Essential metadata methods align with PR objectives!

The uuid and set_metadata methods are crucial for the PR's goal of uploading world metadata only when changed. These methods will help track and update metadata states appropriately.

Consider implementing a metadata versioning mechanism to track changes effectively.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 9ffe3e7 and bb6067b.

📒 Files selected for processing (2)
  • bin/sozo/tests/test_data/policies.json (1 hunks)
  • scripts/rust_fmt.sh (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/rust_fmt.sh
🔇 Additional comments (6)
bin/sozo/tests/test_data/policies.json (6)

3-12: Ohayo! Core system upgrade policies look good, sensei!

The upgrade methods for core system components are properly configured with their respective target addresses.


15-44: Player-related operations are well-structured!

All player-related operations (spawn, move, config management, etc.) are consistently mapped to the same target address, following good practice for cohesive functionality grouping.


55-84: Registration and upgrade operations are properly segregated!

The policy configuration clearly separates registration operations (namespace, event, model, contract) from their corresponding upgrade operations, providing good operational clarity.


87-108: Entity management operations follow consistent patterns!

The entity-related operations (emit, set, delete) are implemented in both singular and plural forms, providing flexible granularity for entity management.


111-124: Access control policies are well-defined!

The owner and writer permission management methods are properly configured, ensuring secure access control for metadata operations.

✅ Verification successful

The initial search revealed the grant/revoke functions, but we need to verify the permission checks and error handling to ensure they're properly implemented. This script will help us understand the complete access control mechanism.


Ohayo sensei! After analyzing the codebase, I can confidently verify the access control implementation.

Access control implementation is robust and secure! ✨

The implementation includes:

  • Proper permission checks through assert_caller_permissions
  • Clear separation between Owner and Writer permissions
  • Comprehensive test coverage for all access control scenarios
  • Protection against malicious contracts and unauthorized access
  • Resource-based permission model with proper validation
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify access control implementation

# Search for access control checks in the codebase
echo "Checking access control implementations..."
rg -A 5 "fn (grant|revoke)_(owner|writer)"

Length of output: 8897


Script:

#!/bin/bash
# Let's check for permission checks and error handling

# Check for permission-related error handling
echo "Checking permission-related error handling..."
rg -A 5 "assert_caller_permissions"

# Check for permission enums/types
echo "Checking permission types..."
rg -A 5 "enum.*Permission"

# Check for permission checks in tests
echo "Checking permission-related tests..."
rg -A 10 "test.*permission|test.*(owner|writer)"

Length of output: 52818


47-124: Verify the comprehensive world metadata operations, sensei!

The new target address (0x52ee...) consolidates all world metadata and entity management operations, which aligns well with the PR's objective of improving metadata handling. However, we should verify that all these methods are actually implemented in the contract.

✅ Verification successful

Ohayo! All world metadata operations are properly implemented, sensei!

The verification shows that all methods listed in the policies.json are fully implemented in the world contract. Specifically:

  • Core metadata methods: uuid, set_metadata, register_namespace, register_event, register_model, register_contract, init_contract
  • Upgrade operations: upgrade_event, upgrade_model, upgrade_contract
  • Entity management: set_entity, set_entities, delete_entity, delete_entities
  • Event handling: emit_event, emit_events
  • Access control: grant_owner, revoke_owner, grant_writer, revoke_writer

All implementations are found in crates/dojo/core/src/world/world_contract.cairo and properly exposed through the interface in crates/dojo/core/src/world/iworld.cairo.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that all methods exist in the contract at the specified target address

# Get all methods from the target contract
echo "Checking methods for target: 0x52ee4d3cba58d1a0462bbfb6813bf5aa1b35078c3b859cded2b727c1d9469ea"

# Search for method declarations in the codebase
methods=(
    "uuid"
    "set_metadata"
    "register_namespace"
    "register_event"
    "register_model"
    "register_contract"
    "init_contract"
    "upgrade_event"
    "upgrade_model"
    "upgrade_contract"
    "emit_event"
    "emit_events"
    "set_entity"
    "set_entities"
    "delete_entity"
    "delete_entities"
    "grant_owner"
    "revoke_owner"
    "grant_writer"
    "revoke_writer"
)

for method in "${methods[@]}"; do
    echo "Searching for method: $method"
    ast-grep --pattern "fn $method" 
done

Length of output: 8897


Script:

#!/bin/bash
# Let's check for the remaining methods in the world contract
echo "Ohayo! Checking world contract implementations..."

# Search for metadata and registration methods
rg -A 5 'fn (uuid|set_metadata|register_(namespace|event|model|contract)|init_contract)' 

# Search for upgrade methods
echo -e "\nChecking upgrade methods..."
rg -A 5 'fn upgrade_(event|model|contract)'

# Search for entity and event methods
echo -e "\nChecking entity and event methods..."
rg -A 5 'fn (emit_event|emit_events|set_entity|set_entities|delete_entity|delete_entities)'

Length of output: 41160

remybar added a commit to remybar/book that referenced this pull request Nov 19, 2024
Update the Metadata page according to dojoengine/dojo#2691.
Copy link
Collaborator

@glihm glihm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking very good @remybar!

Some comments from the first pass of the review, I may drop more comments tomorrow after the functional review. 👍

bin/sozo/src/commands/dev.rs Outdated Show resolved Hide resolved
bin/sozo/src/commands/migrate.rs Outdated Show resolved Hide resolved
crates/dojo/world/src/metadata/fake_metadata_service.rs Outdated Show resolved Hide resolved
crates/dojo/world/src/metadata/ipfs_service.rs Outdated Show resolved Hide resolved
crates/dojo/world/src/remote/events_to_remote.rs Outdated Show resolved Hide resolved
crates/sozo/ops/src/migrate/mod.rs Outdated Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Outside diff range and nitpick comments (23)
crates/dojo/world/src/config/mod.rs (1)

3-3: Consider enhancing IPFS integration resilience, sensei!

Since this configuration will be used for metadata uploads, consider implementing:

  1. Retry mechanisms for failed IPFS uploads
  2. Local caching of metadata to prevent redundant uploads
  3. Circuit breaker pattern for IPFS service failures

Would you like me to provide example implementations for any of these patterns?

Also applies to: 12-12

crates/dojo/world/src/lib.rs (1)

12-12: The services module addition perfectly aligns with our objectives, sensei!

The new services module is a crucial addition that supports the PR's goal of improving metadata upload functionality. This modular approach will help manage IPFS-related operations more effectively, especially for handling world metadata uploads after failed deployments.

Consider documenting the service interfaces in the module-level documentation to help other developers understand the available functionality and integration points.

crates/dojo/world/src/config/ipfs_config.rs (1)

1-22: Consider these architectural improvements

  1. Environment Variables: Consider supporting configuration through environment variables for secure deployment practices.
  2. Secrets Management: Consider integrating with a secrets management solution for production deployments.
  3. Testing: Add unit tests covering validation edge cases and integration tests with mock IPFS endpoints.

Would you like me to help with:

  1. Adding environment variable support using envy crate?
  2. Creating a comprehensive test suite?
  3. Adding documentation about secure configuration practices?
crates/dojo/world/src/services/upload_service.rs (2)

1-6: Ohayo! The trait design looks solid, sensei! A few suggestions for consideration.

The trait is well-designed with proper marker traits (Send + Sync + Unpin) which are essential for async operations. The use of #[allow(async_fn_in_trait)] is necessary as async functions in traits are still unstable in Rust.

Consider using a custom error type instead of anyhow::Result for more specific error handling:

-use anyhow::Result;
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum UploadError {
+    #[error("Upload failed: {0}")]
+    UploadFailed(String),
+    #[error("Invalid URI: {0}")]
+    InvalidUri(String),
+}
+
+pub type Result<T> = std::result::Result<T, UploadError>;

7-15: The upload method signature might be too restrictive, sensei.

The method requires &mut self which could limit concurrent uploads. Consider whether mutability is truly needed for your implementations.

-async fn upload(&mut self, data: Vec<u8>) -> Result<String>;
+async fn upload(&self, data: Vec<u8>) -> Result<String>;
crates/dojo/world/src/services/mock_upload_service.rs (2)

8-14: Consider adding a usage example in the documentation, sensei!

While the documentation clearly explains the purpose, a code example would make it even more helpful for other developers.

Here's a suggested addition to the documentation:

 /// Mock implementation of UploadService 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.
+/// 
+/// # Example
+/// ```
+/// # use your_crate::MockUploadService;
+/// # use your_crate::UploadService;
+/// # async fn example() -> anyhow::Result<()> {
+///     let mut upload_service = MockUploadService::default();
+///     let data = b"Hello, World!".to_vec();
+///     let uri = upload_service.upload(data.clone()).await?;
+///     let retrieved = upload_service.get(uri).await?;
+///     assert_eq!(data, retrieved);
+/// #   Ok(())
+/// # }
+/// ```

18-27: Consider using a more robust hashing mechanism, sensei!

DefaultHasher is not cryptographically secure and could potentially lead to collisions. For a more robust implementation, consider using SHA-256 or another cryptographic hash function.

Here's a suggested improvement:

+use sha2::{Sha256, Digest};
+
 async fn upload(&mut self, data: Vec<u8>) -> Result<String> {
-    let mut hasher = DefaultHasher::new();
-    data.hash(&mut hasher);
-    let hash = hasher.finish();
+    let mut hasher = Sha256::new();
+    hasher.update(&data);
+    let hash = hasher.finalize();
 
-    let uri = format!("ipfs://{:x}", hash);
+    let uri = format!("ipfs://{:x}", hash);
     self.data.insert(uri.clone(), data);
crates/dojo/world/src/config/environment.rs (1)

15-15: Ohayo! Consider adding a getter method for consistency

While the ipfs_config field addition looks good, other fields in this struct have corresponding getter methods. For consistency with the existing pattern, consider adding a getter method.

impl Environment {
+    pub fn ipfs_config(&self) -> Option<&IpfsConfig> {
+        self.ipfs_config.as_ref()
+    }

    // existing methods...
}
bin/sozo/src/commands/options/ipfs.rs (2)

12-13: Enhance help messages with more details

The help messages could be more descriptive to guide users better:

  • IPFS URL: Add expected format and default port
  • Username: Mention if this is the IPFS service credentials
  • Password: Add security recommendations
-    #[arg(help = "The IPFS URL.")]
+    #[arg(help = "The IPFS service URL (e.g., http://localhost:5001)")]

-    #[arg(help = "The IPFS username.")]
+    #[arg(help = "Username for authenticating with the IPFS service")]

-    #[arg(help = "The IPFS password.")]
+    #[arg(help = "Password for IPFS service (prefer using IPFS_PASSWORD_ENV_VAR environment variable)")]

Also applies to: 18-19, 24-25


30-42: Improve error handling and reduce cloning

Ohayo sensei! A few suggestions to enhance this implementation:

  1. Add detailed error messages about missing fields
  2. Avoid unnecessary cloning of Option
 pub fn config(&self) -> Option<IpfsConfig> {
     trace!("Retrieving IPFS config for IpfsOptions.");
 
-    let url = self.ipfs_url.as_ref().map(|url| url.to_string());
-    let username = self.ipfs_username.clone();
-    let password = self.ipfs_password.clone();
+    // Early returns with helpful messages
+    let url = self.ipfs_url.as_ref().map(|url| url.to_string()).ok_or_else(|| {
+        trace!("Missing IPFS URL");
+        ()
+    }).ok()?;
+    
+    let username = self.ipfs_username.as_ref().map(String::as_str).ok_or_else(|| {
+        trace!("Missing IPFS username");
+        ()
+    }).ok()?;
+    
+    let password = self.ipfs_password.as_ref().map(String::as_str).ok_or_else(|| {
+        trace!("Missing IPFS password");
+        ()
+    }).ok()?;
 
-    if let (Some(url), Some(username), Some(password)) = (url, username, password) {
-        Some(IpfsConfig { url, username, password })
-    } else {
-        None
-    }
+    Some(IpfsConfig {
+        url,
+        username: username.to_string(),
+        password: password.to_string(),
+    })
 }
crates/dojo/world/src/metadata/metadata_storage.rs (4)

39-47: Consider using custom error types for better error handling

Ohayo, sensei! The current error handling could be improved by using custom error types instead of using the generic Result type. This would make it easier to handle specific error cases and provide better error messages.

#[derive(Debug, thiserror::Error)]
pub enum MetadataError {
    #[error("Failed to read file: {0}")]
    FileReadError(#[from] std::io::Error),
    #[error("Failed to upload to IPFS: {0}")]
    UploadError(String),
}

async fn upload_uri(uri: &Option<Uri>, service: &mut impl UploadService) -> Result<Option<Uri>, MetadataError> {
    if let Some(Uri::File(path)) = uri {
        let data = std::fs::read(path).map_err(MetadataError::FileReadError)?;
        let uploaded_uri = Uri::Ipfs(service.upload(data).await.map_err(|e| MetadataError::UploadError(e.to_string()))?);
        Ok(Some(uploaded_uri))
    } else {
        Ok(uri.clone())
    }
}

49-51: Enhance trait documentation with examples and implementation guidelines

Ohayo, sensei! The trait documentation could be more comprehensive by including:

  • Usage examples
  • Implementation guidelines
  • Explanation of the hashing mechanism
  • Best practices for implementors
/// Trait to be implemented by metadata structs that need to be
/// uploadable to a storage system (e.g., IPFS).
///
/// # Examples
///
/// ```rust
/// # use your_crate::{MetadataStorage, UploadService};
/// struct MyMetadata {
///     name: String,
///     description: Option<String>,
/// }
///
/// #[async_trait]
/// impl MetadataStorage for MyMetadata {
///     async fn upload(&self, service: &mut impl UploadService) -> Result<String> {
///         // Implementation details...
///     }
/// }
/// ```
///
/// # Implementation Guidelines
///
/// 1. Ensure your type implements `std::hash::Hash` for change detection
/// 2. Handle file uploads appropriately in the `upload` method
/// 3. Use proper error handling with specific error types
/// 4. Consider caching mechanisms for expensive operations

90-103: Reduce code duplication in trait implementations

Ohayo, sensei! The upload implementations for WorldMetadata and ResourceMetadata share similar logic. Consider extracting the common functionality into a helper function.

async fn upload_metadata<T: serde::Serialize>(
    meta: &mut T,
    icon_uri: &Option<Uri>,
    service: &mut impl UploadService,
) -> Result<String> {
    if let Some(icon) = icon_uri {
        meta.set_icon_uri(upload_uri(icon, 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")
}

Then use it in both implementations:

impl MetadataStorage for WorldMetadata {
    async fn upload(&self, service: &mut impl UploadService) -> Result<String> {
        let mut meta = self.clone();
        upload_metadata(&mut meta, &self.icon_uri, service).await
    }
}

Also applies to: 105-116


90-116: Add unit tests for metadata storage implementations

Ohayo, sensei! The implementations would benefit from comprehensive unit tests to verify:

  • Successful metadata uploads
  • Hash comparison logic
  • Error handling
  • URI processing

Would you like me to help generate a test suite for these implementations?

bin/sozo/src/commands/migrate.rs (1)

88-99: Consider enhancing error handling for metadata upload

Ohayo sensei! While the metadata upload implementation looks good, we might want to make it more resilient by providing more detailed error information to users.

Consider this enhancement:

 if let Some(config) = ipfs_config {
     let mut metadata_service = IpfsService::new(config)?;
-    migration
-        .upload_metadata(&mut spinner, &mut metadata_service)
-        .await
-        .context("Metadata upload failed.")?;
+    match migration
+        .upload_metadata(&mut spinner, &mut metadata_service)
+        .await
+    {
+        Ok(_) => {
+            metadata_upload_text = "\nMetadata: Successfully uploaded to IPFS".to_string();
+        }
+        Err(e) => {
+            metadata_upload_text = format!("\nMetadata: Upload failed - {}", e);
+            // Log the full error for debugging
+            tracing::error!("Metadata upload error: {:?}", e);
+        }
+    }
 } else {
     metadata_upload_text = "\nMetadata: No IPFS credentials has been found => \
                             metadata upload has been ignored."
         .to_string();
 };
bin/sozo/src/commands/auth.rs (1)

Line range hint 1-803: Architecture suggestion for future improvements

While the current changes improve the code by standardizing world-level permissions, consider these future enhancements:

  1. Consider adding a dedicated type for resource tags to encapsulate the special handling of "world".
  2. Consider adding validation for permission combinations that might not make sense for the world resource.
crates/sozo/ops/src/migrate/mod.rs (2)

116-170: Consider extracting world metadata handling into a separate method

Ohayo sensei! The world metadata handling logic (lines 125-138) could be extracted into a private method for better organization and reusability.

Consider this refactor:

+ async fn upload_world_metadata(
+     &self,
+     service: &mut impl UploadService,
+     invoker: &mut Invoker<&A>,
+ ) -> anyhow::Result<()> {
+     let current_hash =
+         self.diff.resources.get(&WORLD).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(service, current_hash).await?;
+
+     if let Some((new_uri, new_hash)) = res {
+         invoker.add_call(self.world.set_metadata_getcall(&ResourceMetadata {
+             resource_id: Felt::ZERO,
+             metadata_uri: ByteArray::from_string(&new_uri)?,
+             metadata_hash: new_hash,
+         }));
+     }
+     Ok(())
+ }

158-167: Enhance error handling specificity

The error handling could be more specific to help with debugging.

Consider this enhancement:

     if self.do_multicall() {
         ui.update_text_boxed(format!("Uploading {} metadata...", invoker.calls.len()));
-        invoker.multicall().await.map_err(|e| anyhow!(e.to_string()))?;
+        invoker.multicall().await.map_err(|e| anyhow!("Failed to execute multicall for metadata upload: {}", e))?;
     } else {
         ui.update_text_boxed(format!(
             "Uploading {} metadata (sequentially)...",
             invoker.calls.len()
         ));
-        invoker.invoke_all_sequentially().await.map_err(|e| anyhow!(e.to_string()))?;
+        invoker.invoke_all_sequentially().await.map_err(|e| anyhow!("Failed to execute sequential calls for metadata upload: {}", e))?;
     }
crates/dojo/world/src/services/ipfs_service.rs (5)

1-1: Remove unnecessary import of fmt::write.

Ohayo, sensei! The write! macro used in your Debug implementation doesn't require importing std::fmt::write. You can clean up the code by removing this import.

Apply this diff to remove the unnecessary import:

-use std::{fmt::write, io::Cursor};
+use std::{io::Cursor};

25-25: Correct typos in documentation comments.

Ohayo, sensei! There are a few minor typos in your comments:

  • In line 25, "Instanciate" should be "Instantiate".
  • In lines 31-32, "instanciated" should be "instantiated", and "or a Anyhow error" should be "or an Anyhow error".

Apply this diff to correct the typos:

 /// Instantiate a new IPFS service with IPFS configuration.
 ///
 /// # Arguments
 ///   * `config` - The IPFS configuration
 ///
 /// # Returns
-///  A new `IpfsService` is the IPFS client has been successfully
-///  instanciated or a Anyhow error if not.
+///  Returns a new `IpfsService` if the IPFS client has been successfully
+///  instantiated, or an Anyhow error if not.

Also applies to: 31-32


18-22: Simplify Debug implementation using #[derive(Debug)].

Ohayo, sensei! Since IpfsService contains only one field, you can simplify your code by deriving the Debug trait instead of implementing it manually.

Apply this diff to use #[derive(Debug)]:

+#[derive(Debug)]
 pub struct IpfsService {
     client: ipfs_api_backend_hyper::IpfsClient,
 }

-// impl required by clippy
-impl std::fmt::Debug for IpfsService {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
-        write!(f, "IPFS service")?;
-        Ok(())
-    }
-}

45-53: Check error handling for IPFS upload failures.

Ohayo, sensei! The error handling in the upload method is good. However, consider including more context in the error message to aid in debugging, such as the data size or a hint to check network connectivity.

Optionally, you can enhance the error message like this:

 .await
-.map_err(|e| anyhow::anyhow!("Failed to upload to IPFS: {}", e))?;
+.map_err(|e| anyhow::anyhow!("Failed to upload {} bytes to IPFS: {}. Please check your network connection and IPFS daemon.", data.len(), e))?;

55-64: Ensure get method is secure and handles errors appropriately.

Ohayo, sensei! In the test-only get method, consider handling cases where the URI might not be properly formatted or the content retrieval fails, to make tests more robust.

You might add validation for the URI and improve error messages:

 async fn get(&self, uri: String) -> Result<Vec<u8>> {
+    if !uri.starts_with("ipfs://") {
+        return Err(anyhow::anyhow!("Invalid IPFS URI: {}", uri));
+    }
     let res = self
         .client
         .cat(&uri.replace("ipfs://", ""))
         .map_ok(|chunk| chunk.to_vec())
         .try_concat()
         .await
-        ?;
+        .map_err(|e| anyhow::anyhow!("Failed to retrieve from IPFS: {}", e))?;
     Ok(res)
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between bb6067b and d5e1ea5.

⛔ Files ignored due to path filters (5)
  • crates/torii/types-test/Scarb.lock is excluded by !**/*.lock
  • examples/simple/Scarb.lock is excluded by !**/*.lock
  • examples/spawn-and-move/Scarb.lock is excluded by !**/*.lock
  • spawn-and-move-db.tar.gz is excluded by !**/*.gz
  • types-test-db.tar.gz is excluded by !**/*.gz
📒 Files selected for processing (20)
  • bin/sozo/src/commands/auth.rs (5 hunks)
  • bin/sozo/src/commands/dev.rs (2 hunks)
  • bin/sozo/src/commands/migrate.rs (6 hunks)
  • bin/sozo/src/commands/options/ipfs.rs (1 hunks)
  • crates/dojo/world/src/config/environment.rs (2 hunks)
  • crates/dojo/world/src/config/ipfs_config.rs (1 hunks)
  • crates/dojo/world/src/config/mod.rs (1 hunks)
  • crates/dojo/world/src/config/profile_config.rs (6 hunks)
  • crates/dojo/world/src/constants.rs (1 hunks)
  • crates/dojo/world/src/lib.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_storage.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test.rs (1 hunks)
  • crates/dojo/world/src/metadata/mod.rs (1 hunks)
  • crates/dojo/world/src/remote/events_to_remote.rs (5 hunks)
  • crates/dojo/world/src/services/ipfs_service.rs (1 hunks)
  • crates/dojo/world/src/services/mock_upload_service.rs (1 hunks)
  • crates/dojo/world/src/services/mod.rs (1 hunks)
  • crates/dojo/world/src/services/upload_service.rs (1 hunks)
  • crates/sozo/ops/src/migrate/mod.rs (2 hunks)
  • crates/sozo/ops/src/tests/migration.rs (4 hunks)
✅ Files skipped from review due to trivial changes (2)
  • crates/dojo/world/src/constants.rs
  • crates/dojo/world/src/services/mod.rs
🚧 Files skipped from review as they are similar to previous changes (6)
  • bin/sozo/src/commands/dev.rs
  • crates/dojo/world/src/config/profile_config.rs
  • crates/dojo/world/src/metadata/metadata_test.rs
  • crates/dojo/world/src/metadata/mod.rs
  • crates/dojo/world/src/remote/events_to_remote.rs
  • crates/sozo/ops/src/tests/migration.rs
🔇 Additional comments (17)
crates/dojo/world/src/config/mod.rs (1)

3-3: Ohayo! Clean module structure for new configurations, sensei!

The addition of IPFS and resource configurations follows good modular design principles and aligns well with the metadata upload requirements.

Let's verify the module structure is complete:

Also applies to: 8-8, 12-12, 16-16

✅ Verification successful

Ohayo! Module structure is perfectly balanced, sensei!

The verification shows that both modules are properly implemented and well-integrated:

  • ipfs_config.rs provides a robust configuration with validation for IPFS credentials
  • resource_config.rs defines a clean structure for resource metadata
  • Both configs are actively used throughout the codebase in appropriate contexts (ipfs_service, profile_config, metadata_config)
  • The module exports are properly consumed by dependent modules
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the module structure and ensure all necessary files exist

# Check if the new module files exist
fd --type f "ipfs_config.rs|resource_config.rs" crates/dojo/world/src/config/

# Verify no orphaned imports (modules declared but files missing)
for module in ipfs_config resource_config; do
  if ! fd --type f "${module}.rs" crates/dojo/world/src/config/ >/dev/null; then
    echo "Warning: ${module}.rs is missing but declared in mod.rs"
  fi
done

# Check for proper module exports usage
rg --type rust "use (IpfsConfig|ResourceConfig)" crates/dojo

Length of output: 444


Script:

#!/bin/bash
# Let's check the content of these module files to verify their structure
echo "=== ipfs_config.rs ==="
cat crates/dojo/world/src/config/ipfs_config.rs

echo -e "\n=== resource_config.rs ==="
cat crates/dojo/world/src/config/resource_config.rs

# Let's also check if these configs are being used in the codebase
echo -e "\n=== Usage of configs ==="
rg -A 2 "IpfsConfig|ResourceConfig" crates/dojo/world/src/

Length of output: 4440

crates/dojo/world/src/lib.rs (1)

7-7: Ohayo! New constants module looks good, but let's verify its contents.

The addition of a dedicated constants module follows good practices for maintainability.

Let's check what constants are being defined:

✅ Verification successful

Ohayo! The constants module looks well-organized and relevant, sensei!

The constants module contains a comprehensive set of well-structured constants that are essential for the world metadata handling, including:

  • Database table names and column definitions
  • Type names with proper namespacing to avoid conflicts
  • Metadata-specific constants like METADATA_TABLE, METADATA_TYPE_NAME, and METADATA_NAMES
  • Additional infrastructure constants for gas prices, addresses, and world configuration
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check the contents of the constants module
# Expected: Constants related to metadata handling

# Check the constants file content
fd -t f "constants.rs" -x cat {}

Length of output: 4135

crates/dojo/world/src/services/upload_service.rs (1)

17-26: LGTM! Well-structured test utility method.

The get method is properly cfg-gated for tests and well-documented. This will be useful for testing implementations of the trait.

crates/dojo/world/src/services/mock_upload_service.rs (1)

1-6: Ohayo! Clean and well-organized imports, sensei!

The imports are minimal, specific, and well-structured, showing good practices in dependency management.

crates/dojo/world/src/config/environment.rs (1)

3-4: LGTM! Clean import addition, sensei!

The import is well-placed and follows the existing module structure.

crates/dojo/world/src/metadata/metadata_storage.rs (1)

70-87: Implementation aligns well with PR objectives

Ohayo, sensei! The upload_if_changed method successfully addresses the core objectives of PR #2691:

  1. Implements change detection using metadata hashing
  2. Allows for metadata re-upload after failed deployments
  3. Optimizes uploads by only processing changed metadata

Let's verify the change detection logic:

bin/sozo/src/commands/migrate.rs (4)

6-6: LGTM! Clean import additions

Ohayo! The new imports for IPFS functionality are well-organized and necessary for the implementation.

Also applies to: 18-18


37-39: LGTM! Clean struct extension

The addition of IpfsOptions follows the established pattern of flattened command options.


51-51: LGTM! Clean config handling

The IPFS configuration is properly retrieved with a good fallback chain from command line to profile config.

Also applies to: 84-85


107-121: LGTM! Clean output formatting

The metadata upload status is nicely integrated into the existing output format, maintaining consistency with the codebase's style.

bin/sozo/src/commands/auth.rs (5)

10-10: Ohayo! Nice improvement in constant usage!

The introduction of the WORLD constant from dojo_world::constants enhances code clarity and maintainability by replacing magic values.


309-314: Consistent world resource handling in clone_permissions, sensei!

The replacement of hardcoded hex formatting with a conditional check for WORLD improves readability and maintainability. The special case handling for the "World" string representation is more user-friendly.


421-428: Clean world permissions retrieval implementation!

The use of WORLD constant in permission retrieval makes the code more maintainable and consistent with the rest of the codebase.


792-792: Test case properly validates WORLD constant usage!

The test case has been appropriately updated to use the WORLD constant, ensuring the new behavior is properly tested.


681-681: Elegant selector resolution in PermissionPair!

The special case handling for "world" resource tag using the WORLD constant improves code clarity. However, let's verify that this change doesn't break existing functionality.

✅ Verification successful

WORLD constant usage is properly integrated and tested, sensei! ✨

The verification shows that the WORLD constant is correctly used across multiple contexts:

  • Used in permission pair resolution with proper test coverage
  • Properly handled in string formatting for writer/owner displays
  • Correctly integrated with world diff functionality
  • Has test assertions verifying its behavior

The special case handling maintains consistency with existing functionality and improves code clarity.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify that the world resource tag is handled correctly in existing code

# Search for any direct usage of "world" as a resource tag
rg -l 'resource_tag.*"world"' 

# Search for any test cases involving world permissions
rg -l 'test.*world.*permission'

Length of output: 95


Script:

#!/bin/bash
# Let's check for WORLD constant usage and its context
rg -A 5 -B 5 'WORLD' bin/sozo/src/commands/auth.rs

# Check for any world-related permission tests or functionality
ast-grep --pattern 'fn $_ () {
  $$$
  WORLD
  $$$
}'

# Look for any permission-related constants
rg -A 2 'const.*Permission'

Length of output: 2574

crates/sozo/ops/src/migrate/mod.rs (2)

172-199: LGTM! Well-structured metadata handling implementation

The implementation is clean and follows good practices:

  • Uses slices instead of &Vec for better efficiency
  • Properly handles metadata changes
  • Clear and maintainable logic flow

180-181: 🛠️ Refactor suggestion

Add error handling for selector computation

Ohayo sensei! The selector computation could fail silently. Consider adding explicit error handling.

Apply this diff:

-            let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag);
+            let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag)
+                .map_err(|e| anyhow!("Failed to compute selector for tag {}: {}", item.tag, e))?;

Likely invalid or redundant comment.

Comment on lines +4 to +9
#[derive(Default, Deserialize, Clone, Debug)]
pub struct IpfsConfig {
pub url: String,
pub username: String,
pub password: String,
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo! Consider enhancing security for credential handling

The current implementation might expose sensitive credentials:

  1. Debug derive could leak credentials in logs
  2. Default implementation creates empty credentials
  3. Credentials stored as plain strings in memory

Consider these improvements:

-#[derive(Default, Deserialize, Clone, Debug)]
+#[derive(Deserialize, Clone)]
+#[serde(deny_unknown_fields)]
 pub struct IpfsConfig {
     pub url: String,
-    pub username: String,
-    pub password: String
+    #[serde(sensitive)]
+    pub username: String,
+    #[serde(sensitive)]
+    pub password: String
 }

+impl std::fmt::Debug for IpfsConfig {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("IpfsConfig")
+            .field("url", &self.url)
+            .field("username", &"[REDACTED]")
+            .field("password", &"[REDACTED]")
+            .finish()
+    }
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[derive(Default, Deserialize, Clone, Debug)]
pub struct IpfsConfig {
pub url: String,
pub username: String,
pub password: String,
}
#[derive(Deserialize, Clone)]
#[serde(deny_unknown_fields)]
pub struct IpfsConfig {
pub url: String,
#[serde(sensitive)]
pub username: String,
#[serde(sensitive)]
pub password: String
}
impl std::fmt::Debug for IpfsConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IpfsConfig")
.field("url", &self.url)
.field("username", &"[REDACTED]")
.field("password", &"[REDACTED]")
.finish()
}
}

Comment on lines +12 to +21
pub fn assert_valid(&self) -> Result<()> {
if self.url.is_empty() || self.username.is_empty() || self.password.is_empty() {
anyhow::bail!("Invalid IPFS credentials: empty values not allowed");
}
if !self.url.starts_with("http://") && !self.url.starts_with("https://") {
anyhow::bail!("Invalid IPFS URL: must start with http:// or https://");
}

Ok(())
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance validation robustness, sensei!

The current validation could be more comprehensive:

  1. URL validation only checks the scheme
  2. No validation for malformed URLs
  3. No validation for username/password format

Consider this enhanced validation:

 pub fn assert_valid(&self) -> Result<()> {
     if self.url.is_empty() || self.username.is_empty() || self.password.is_empty() {
         anyhow::bail!("Invalid IPFS credentials: empty values not allowed");
     }
-    if !self.url.starts_with("http://") && !self.url.starts_with("https://") {
-        anyhow::bail!("Invalid IPFS URL: must start with http:// or https://");
+    
+    // Validate and normalize URL
+    let url = url::Url::parse(&self.url)
+        .map_err(|e| anyhow::anyhow!("Invalid IPFS URL: {}", e))?;
+    
+    if url.scheme() != "http" && url.scheme() != "https" {
+        anyhow::bail!("Invalid IPFS URL: scheme must be http or https");
     }
+    
+    // Validate username format (example: alphanumeric only)
+    if !self.username.chars().all(|c| c.is_alphanumeric() || c == '_') {
+        anyhow::bail!("Invalid username: must be alphanumeric");
+    }
+    
+    // Validate password strength
+    if self.password.len() < 8 {
+        anyhow::bail!("Invalid password: must be at least 8 characters");
+    }

     Ok(())
 }

Don't forget to add the url crate to your dependencies:

 [dependencies]
+url = "2.5.0"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn assert_valid(&self) -> Result<()> {
if self.url.is_empty() || self.username.is_empty() || self.password.is_empty() {
anyhow::bail!("Invalid IPFS credentials: empty values not allowed");
}
if !self.url.starts_with("http://") && !self.url.starts_with("https://") {
anyhow::bail!("Invalid IPFS URL: must start with http:// or https://");
}
Ok(())
}
pub fn assert_valid(&self) -> Result<()> {
if self.url.is_empty() || self.username.is_empty() || self.password.is_empty() {
anyhow::bail!("Invalid IPFS credentials: empty values not allowed");
}
// Validate and normalize URL
let url = url::Url::parse(&self.url)
.map_err(|e| anyhow::anyhow!("Invalid IPFS URL: {}", e))?;
if url.scheme() != "http" && url.scheme() != "https" {
anyhow::bail!("Invalid IPFS URL: scheme must be http or https");
}
// Validate username format (example: alphanumeric only)
if !self.username.chars().all(|c| c.is_alphanumeric() || c == '_') {
anyhow::bail!("Invalid username: must be alphanumeric");
}
// Validate password strength
if self.password.len() < 8 {
anyhow::bail!("Invalid password: must be at least 8 characters");
}
Ok(())
}

crates/dojo/world/src/services/mock_upload_service.rs Outdated Show resolved Hide resolved
bin/sozo/src/commands/options/ipfs.rs Show resolved Hide resolved
bin/sozo/src/commands/options/ipfs.rs Outdated Show resolved Hide resolved
bin/sozo/src/commands/migrate.rs Show resolved Hide resolved
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (9)
crates/dojo/world/src/services/ipfs_service.rs (3)

1-14: Ohayo! The struct definition looks clean, sensei!

The imports are well-organized and the struct is minimal. The documentation comment effectively explains the purpose.

Consider adding a code example to the struct documentation to show basic usage:

 /// IPFS implementation of UploadService, allowing to upload data to IPFS.
+/// 
+/// # Example
+/// ```
+/// use dojo_world::config::IpfsConfig;
+/// use dojo_world::services::IpfsService;
+/// 
+/// let config = IpfsConfig::new("http://localhost:5001", "user", "pass");
+/// let service = IpfsService::new(config)?;
+/// ```
 pub struct IpfsService {

16-22: Consider enhancing the Debug implementation, sensei!

The current Debug implementation only prints "IPFS service" which might not be helpful for debugging. Consider including the IPFS URL (with credentials masked) for better debugging context.

Here's a suggested improvement:

 impl std::fmt::Debug for IpfsService {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
-        write!(f, "IPFS service")?;
+        write!(f, "IpfsService {{ url: {} }}", self.client.base_url())?;
         Ok(())
     }
 }

1-65: Excellent service abstraction design, sensei!

The IpfsService implementation aligns perfectly with the PR objectives for handling world metadata uploads. The separation of concerns through the UploadService trait allows for easy testing and potential alternative implementations.

A few architectural considerations:

  1. Consider adding retry logic for failed uploads
  2. Think about implementing a caching layer to avoid re-uploading identical data
  3. Consider adding metrics/logging for monitoring upload performance

Would you like help implementing any of these architectural improvements?

crates/dojo/world/src/metadata/metadata_storage.rs (3)

39-47: Enhance error handling with file-specific context

Ohayo, sensei! While the error handling is functional, it could be more descriptive for debugging purposes.

Consider enhancing the error context:

 async fn upload_uri(uri: &Option<Uri>, service: &mut impl UploadService) -> Result<Option<Uri>> {
     if let Some(Uri::File(path)) = uri {
-        let data = std::fs::read(path)?;
+        let data = std::fs::read(path)
+            .with_context(|| format!("Failed to read file: {}", path.display()))?;
         let uploaded_uri = Uri::Ipfs(service.upload(data).await?);
         Ok(Some(uploaded_uri))
     } else {
         Ok(uri.clone())
     }
 }

70-87: Enhance error handling in upload_if_changed

Ohayo, sensei! The error handling in this method could be improved to provide better context about hash computation and comparison failures.

Consider adding more context to error handling:

     async fn upload_if_changed(
         &self,
         service: &mut impl UploadService,
         current_hash: Felt,
     ) -> Result<Option<(String, Felt)>>
     where
         Self: std::hash::Hash,
     {
-        let new_hash = compute_metadata_hash(self);
-        let new_hash = Felt::from_raw([0, 0, 0, new_hash]);
+        let new_hash = compute_metadata_hash(self);
+        let new_hash = Felt::from_raw([0, 0, 0, new_hash])
+            .context("Failed to convert hash to Felt")?;

         if new_hash != current_hash {
-            let new_uri = self.upload(service).await?;
+            let new_uri = self.upload(service)
+                .await
+                .context("Failed to upload changed metadata")?;
             return Ok(Some((new_uri, new_hash)));
         }

         Ok(None)
     }

91-116: Consider reducing code duplication in metadata implementations

Ohayo, sensei! The WorldMetadata and ResourceMetadata implementations share similar upload logic, which could be abstracted to reduce duplication.

Consider creating a helper function for the common upload logic:

+async fn upload_metadata<T: serde::Serialize>(
+    meta: &mut T,
+    icon_uri: &Option<Uri>,
+    service: &mut impl UploadService
+) -> Result<String> {
+    if let Some(icon_uri) = icon_uri {
+        let new_uri = upload_uri(&Some(icon_uri.clone()), service)
+            .await
+            .context("Failed to upload icon URI")?;
+        // Assuming there's a way to set icon_uri on T
+        // You'll need to define a trait for this
+    }
+    
+    let serialized = json!(meta).to_string();
+    service.upload(serialized.as_bytes().to_vec())
+        .await
+        .context("Failed to upload metadata")
+}

This would simplify both implementations and make future maintenance easier.

bin/sozo/src/commands/auth.rs (1)

681-681: Consider adding validation for world resource permissions.

Ohayo sensei! While the implementation looks correct, consider adding validation to ensure appropriate permission levels when dealing with world resources.

 if self.resource_tag == "world" {
+    // Validate world resource permissions
+    if !is_valid_world_permission(&self.grantee_tag_or_address) {
+        return Err(anyhow!("Invalid permission level for world resource"));
+    }
     WORLD
crates/sozo/ops/src/migrate/mod.rs (2)

111-170: Consider enhancing error handling and code organization

Ohayo sensei! The method is well-structured, but here are some suggestions for improvement:

  1. Consider extracting the multicall logic into a separate helper method to reduce code duplication between lines 158-167.
  2. The error handling at line 160 could be more specific than converting to string.

Here's a suggested refactor for the multicall logic:

+ async fn execute_metadata_calls(&self, invoker: &mut Invoker<&A>, ui: &mut MigrationUi) -> anyhow::Result<()> {
+     if self.do_multicall() {
+         ui.update_text_boxed(format!("Uploading {} metadata...", invoker.calls.len()));
+         invoker.multicall().await.map_err(|e| anyhow!("Multicall failed: {}", e))?;
+     } else {
+         ui.update_text_boxed(format!(
+             "Uploading {} metadata (sequentially)...",
+             invoker.calls.len()
+         ));
+         invoker.invoke_all_sequentially().await.map_err(|e| anyhow!("Sequential calls failed: {}", e))?;
+     }
+     Ok(())
+ }

Then replace lines 158-167 with:

- if self.do_multicall() {
-     ui.update_text_boxed(format!("Uploading {} metadata...", invoker.calls.len()));
-     invoker.multicall().await.map_err(|e| anyhow!(e.to_string()))?;
- } else {
-     ui.update_text_boxed(format!(
-         "Uploading {} metadata (sequentially)...",
-         invoker.calls.len()
-     ));
-     invoker.invoke_all_sequentially().await.map_err(|e| anyhow!(e.to_string()))?;
- }
+ self.execute_metadata_calls(&mut invoker, ui).await?;

172-199: Consider adding early returns for better readability

Ohayo sensei! The method could be more concise with early returns. If no metadata changes are detected, we could return early with an empty vector.

Here's a suggested refactor:

 async fn upload_metadata_from_resource_config(
     &self,
     service: &mut impl UploadService,
     config: &[ResourceConfig],
 ) -> anyhow::Result<Vec<Call>> {
+    if config.is_empty() {
+        return Ok(vec![]);
+    }
+
     let mut calls = vec![];
     
     for item in config {
         let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag);
         
         let current_hash =
             self.diff.resources.get(&selector).map_or(Felt::ZERO, |r| r.metadata_hash());
         
         let new_metadata = metadata_config::ResourceMetadata::from(item.clone());
         
         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 {
                 resource_id: selector,
                 metadata_uri: ByteArray::from_string(&new_uri)?,
                 metadata_hash: new_hash,
             }));
         }
     }
     
     Ok(calls)
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between d5e1ea5 and cdf4b4a.

⛔ Files ignored due to path filters (5)
  • crates/torii/types-test/Scarb.lock is excluded by !**/*.lock
  • examples/simple/Scarb.lock is excluded by !**/*.lock
  • examples/spawn-and-move/Scarb.lock is excluded by !**/*.lock
  • spawn-and-move-db.tar.gz is excluded by !**/*.gz
  • types-test-db.tar.gz is excluded by !**/*.gz
📒 Files selected for processing (24)
  • bin/sozo/src/commands/auth.rs (5 hunks)
  • bin/sozo/src/commands/dev.rs (2 hunks)
  • bin/sozo/src/commands/migrate.rs (6 hunks)
  • bin/sozo/src/commands/options/ipfs.rs (1 hunks)
  • crates/dojo-world/Cargo.toml (1 hunks)
  • crates/dojo/world/Cargo.toml (1 hunks)
  • crates/dojo/world/src/config/environment.rs (2 hunks)
  • crates/dojo/world/src/config/ipfs_config.rs (1 hunks)
  • crates/dojo/world/src/config/mod.rs (1 hunks)
  • crates/dojo/world/src/config/profile_config.rs (6 hunks)
  • crates/dojo/world/src/constants.rs (1 hunks)
  • crates/dojo/world/src/lib.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_storage.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test.rs (1 hunks)
  • crates/dojo/world/src/metadata/mod.rs (1 hunks)
  • crates/dojo/world/src/remote/events_to_remote.rs (5 hunks)
  • crates/dojo/world/src/services/ipfs_service.rs (1 hunks)
  • crates/dojo/world/src/services/mock_upload_service.rs (1 hunks)
  • crates/dojo/world/src/services/mod.rs (1 hunks)
  • crates/dojo/world/src/services/upload_service.rs (1 hunks)
  • crates/sozo/ops/Cargo.toml (1 hunks)
  • crates/sozo/ops/src/migrate/mod.rs (2 hunks)
  • crates/sozo/ops/src/tests/migration.rs (4 hunks)
  • crates/torii/graphql/Cargo.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (18)
  • bin/sozo/src/commands/dev.rs
  • bin/sozo/src/commands/migrate.rs
  • bin/sozo/src/commands/options/ipfs.rs
  • crates/dojo/world/Cargo.toml
  • crates/dojo/world/src/config/environment.rs
  • crates/dojo/world/src/config/ipfs_config.rs
  • crates/dojo/world/src/config/mod.rs
  • crates/dojo/world/src/config/profile_config.rs
  • crates/dojo/world/src/constants.rs
  • crates/dojo/world/src/lib.rs
  • crates/dojo/world/src/metadata/metadata_test.rs
  • crates/dojo/world/src/metadata/mod.rs
  • crates/dojo/world/src/remote/events_to_remote.rs
  • crates/dojo/world/src/services/mock_upload_service.rs
  • crates/dojo/world/src/services/mod.rs
  • crates/dojo/world/src/services/upload_service.rs
  • crates/sozo/ops/Cargo.toml
  • crates/sozo/ops/src/tests/migration.rs
🔇 Additional comments (12)
crates/torii/graphql/Cargo.toml (2)

40-40: Ohayo! The feature change looks good, sensei! 🎉

The switch from "metadata" to "ipfs" feature aligns perfectly with our goal of improving metadata upload handling through IPFS integration.


40-40: Let's verify the feature consistency across the workspace

We should ensure this feature change is consistently applied across all relevant workspace members.

✅ Verification successful

Feature configuration is consistent across workspace, sensei! ✨

The ipfs feature for dojo-world is consistently configured in both places where it's used with workspace inheritance:

  • crates/torii/graphql/Cargo.toml
  • crates/sozo/ops/Cargo.toml

No inconsistencies or missing configurations were found in the workspace.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistency of dojo-world features across workspace

# Search for dojo-world dependency configurations
echo "Checking dojo-world dependency configurations across workspace:"
rg --type toml 'dojo-world.*features' -A 2

# Check if ipfs-related dependencies are properly configured
echo -e "\nChecking for IPFS-related dependencies:"
rg --type toml 'ipfs.*=.*version' -A 1

Length of output: 718


Script:

#!/bin/bash
# Let's check the workspace Cargo.toml to see if dojo-world is defined there
echo "Checking workspace Cargo.toml for dojo-world configuration:"
rg --type toml -A 5 '\[workspace\.dependencies\].*dojo-world' 

# Let's also check all Cargo.toml files that depend on dojo-world
echo -e "\nChecking all dojo-world dependencies across workspace:"
rg --type toml '^dojo-world\s*=' 

# Let's verify if there are any other IPFS-related features
echo -e "\nChecking for any other IPFS-related features:"
rg --type toml 'features.*ipfs'

Length of output: 894

crates/dojo-world/Cargo.toml (2)

54-54: Verify dependency versions for security

Ohayo sensei! The IPFS feature introduces new dependencies. Let's ensure they're using secure versions.

✅ Verification successful

IPFS dependency is securely configured, sensei! ✨

The repository is using a pinned version of ipfs-api-backend-hyper from a specific Git commit (af2c17f7), which is a good security practice. The dependency tree shows well-maintained dependencies including:

  • hyper v0.14 with secure TLS implementation (hyper-rustls)
  • Modern versions of critical dependencies (futures 0.3, http 0.2)
  • Rustls for TLS with native certificates enabled
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for security advisories and latest versions
# Expected: No critical vulnerabilities in the dependencies

# Check for security advisories for ipfs-api-backend-hyper
gh api graphql -f query='
{
  securityVulnerabilities(first: 5, ecosystem: CARGO, package: "ipfs-api-backend-hyper") {
    nodes {
      advisory {
        summary
        severity
        publishedAt
      }
      vulnerableVersionRange
      firstPatchedVersion {
        identifier
      }
    }
  }
}'

# Check latest versions from crates.io
curl -s https://crates.io/api/v1/crates/ipfs-api-backend-hyper | jq '.crate.max_version'

Length of output: 882


Script:

#!/bin/bash
# Let's check the actual version specified in Cargo.toml and scan for any known vulnerabilities using cargo-audit
cat crates/dojo-world/Cargo.toml | rg "ipfs-api-backend-hyper"

# Let's also check dependencies of ipfs-api-backend-hyper for a better security assessment
curl -s https://crates.io/api/v1/crates/ipfs-api-backend-hyper/0.6.0/dependencies | jq '.'

# Check if there are any direct usages that might need security considerations
rg "ipfs-api-backend-hyper" -A 2 -B 2

Length of output: 5969


54-54: ⚠️ Potential issue

Ohayo! Breaking change alert: feature rename from metadata to ipfs

The feature rename from metadata to ipfs better reflects its purpose but could break existing code. Please ensure all dependent crates are updated accordingly.

Let's check for any usage of the old feature name:

✅ Verification successful

Let's also check for any current usage of the new 'ipfs' feature to understand the impact:


Ohayo sensei! The feature rename is safe to proceed!

After careful investigation, I can confirm that there was no previous metadata feature in the codebase. The occurrences of "metadata" in Cargo.toml files are only related to package metadata configurations (like package.metadata.release and package.metadata.cargo-udeps.ignore), not feature flags. The new ipfs feature is properly defined and only used in conditional compilation for the IPFS service module.

The feature rename from metadata to ipfs is actually a new addition rather than a rename, making this a non-breaking change.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for usage of the old 'metadata' feature
# Expected: No occurrences of the old feature name in feature gates or Cargo.toml files

# Search for feature gates using the old name
rg '#\[cfg\(feature\s*=\s*"metadata"\)\]'

# Search for feature specifications in Cargo.toml files
fd -g 'Cargo.toml' --exec rg 'metadata"\s*]|metadata\s*='

Length of output: 101


Script:

#!/bin/bash
# Let's try a more comprehensive search for the old feature name
# Search for 'metadata' feature in Cargo.toml files
fd 'Cargo.toml$' -x grep -l "metadata"

# Search for any code that might be using the feature
rg -g '*.rs' 'metadata' -B2 -A2

# Check git history for the feature rename
git log -p --all --full-history -S "metadata" -- "*Cargo.toml"

Length of output: 142092


Script:

#!/bin/bash
# Search for current usage of 'ipfs' feature
rg -g '*.rs' '#\[cfg\(feature\s*=\s*"ipfs"\)\]' -B2 -A2

# Search for ipfs feature in Cargo.toml files
fd 'Cargo.toml$' -x grep -l "ipfs"

Length of output: 641


Script:

#!/bin/bash
# Let's check if there are any dependencies or code that rely on the old 'metadata' feature
# First, check all Cargo.toml files for any dependencies using the old feature name
fd 'Cargo.toml$' -x grep -A5 -B5 'metadata.*='

# Then check for any feature gates or conditional compilation in the code
rg -g '*.rs' 'metadata.*feature' -B2 -A2

# Finally, let's look at the git history for this specific feature rename
git log -p -- "*Cargo.toml" | grep -A5 -B5 'metadata.*='

Length of output: 91


Script:

#!/bin/bash
# Let's check the specific file and its dependencies more thoroughly
cat crates/dojo-world/Cargo.toml

# Check for any direct references to the old feature name in the workspace
rg -g '!target' -g '!*.lock' 'metadata' --type toml

# Look for any conditional compilation using either feature
rg -g '*.rs' -B2 -A2 '(?:#\[cfg\(feature\s*=\s*"(?:metadata|ipfs)"\)\]|#\[cfg_attr\(.*feature\s*=\s*"(?:metadata|ipfs)")'

Length of output: 2778

crates/dojo/world/src/services/ipfs_service.rs (2)

43-65: 🛠️ Refactor suggestion

Consider adding input validation and improving URI handling, sensei!

The implementation is good, but could be enhanced in a few areas:

  1. The upload method should validate input data size to prevent excessive uploads
  2. The get method's URI manipulation could be more robust

Here's a suggested improvement:

 async fn upload(&mut self, data: Vec<u8>) -> Result<String> {
+    const MAX_UPLOAD_SIZE: usize = 50 * 1024 * 1024; // 50MB
+    if data.len() > MAX_UPLOAD_SIZE {
+        return Err(anyhow::anyhow!("Upload size exceeds maximum allowed size"));
+    }
     let reader = Cursor::new(data);
     let response = self
 #[cfg(test)]
 async fn get(&self, uri: String) -> Result<Vec<u8>> {
+    if !uri.starts_with("ipfs://") {
+        return Err(anyhow::anyhow!("Invalid IPFS URI format"));
+    }
     let res = self
         .client
-        .cat(&uri.replace("ipfs://", ""))
+        .cat(&uri.strip_prefix("ipfs://").unwrap())

Let's check if there are any size limits defined elsewhere in the codebase:


24-41: Fix documentation typos and consider credential handling, sensei!

The implementation looks solid, but there are a few points to address:

  1. Documentation has typos ("instanciate" should be "instantiate")
  2. Consider validating credentials before passing them to the client

Fix the documentation typos:

-    /// Instanciate a new IPFS service with IPFS configuration.
+    /// Instantiate a new IPFS service with IPFS configuration.
     ///
     /// # Arguments
     ///   * `config` - The IPFS configuration
     ///
     /// # Returns
-    ///  A new `IpfsService` is the IPFS client has been successfully
-    ///  instanciated or a Anyhow error if not.
+    ///  A new `IpfsService` if the IPFS client has been successfully
+    ///  instantiated or an Anyhow error if not.

Let's verify if there are any credential validation patterns in the codebase:

✅ Verification successful

Fix documentation typos and validate credentials properly, sensei!

  1. The documentation typos should be fixed as suggested.
  2. The credentials are already being validated in assert_valid() which checks for:
    • Empty values in url, username, and password
    • Valid URL format (must start with http:// or https://)
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for credential validation patterns
rg -i "credentials?.*(validate|check|verify)" 

Length of output: 47


Script:

#!/bin/bash
# Let's check the IpfsConfig implementation for credential validation
ast-grep --pattern 'impl IpfsConfig {
  $$$
  assert_valid($$$) {
    $$$
  }
  $$$
}'

# Also check for any credential-related validation in the config
rg -B2 -A5 "assert_valid|validate.*credentials?"

# Let's also check the IpfsConfig struct definition
ast-grep --pattern 'struct IpfsConfig {
  $$$
}'

Length of output: 173952

crates/dojo/world/src/metadata/metadata_storage.rs (1)

1-116: Implementation successfully addresses the PR objectives

Ohayo, sensei! The implementation effectively solves the issues mentioned in #2096 and #1907 by:

  1. Implementing change detection through hashing
  2. Supporting metadata re-upload after failed deployments
  3. Adding proper error handling and context

The code is well-structured and maintainable, with room for minor improvements as suggested above.

bin/sozo/src/commands/auth.rs (4)

10-10: LGTM! Clean import of the WORLD constant.

The import of WORLD from dojo_world::constants aligns with the PR objectives to improve world metadata handling.


421-428: LGTM! Clean migration from Felt::ZERO to WORLD constant.

The change from Felt::ZERO to WORLD in permission retrieval improves code readability and maintainability.


792-792: LGTM! Test case updated correctly.

The test case has been properly updated to use the new WORLD constant instead of Felt::ZERO.


309-314: Verify the string representation consistency.

The string representation logic for world resources looks good, but let's ensure consistent handling across the codebase.

✅ Verification successful

Let me search for more instances of world resource string representations to ensure we have a complete picture.


String representation of world resources is consistent, sensei!

The code shows consistent handling of world resources across the codebase. In auth.rs, the pattern if r != &WORLD { format!("{:#066x}", r) } else { "World".to_string() } is used consistently for world resource string representation. Other usages of WORLD and Felt::ZERO string conversions in the codebase serve different purposes and don't conflict with this pattern.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent world resource string representation
# Test: Search for other instances where world resources are converted to strings
rg -A 2 'if \S+ != &(WORLD|Felt::ZERO).*\{.*to_string\(\)' 

Length of output: 467


Script:

#!/bin/bash
# Search for any other string representations of WORLD or similar constants
rg -A 2 'WORLD.*to_string|Felt::ZERO.*to_string'

# Also check for format! calls with WORLD
rg -A 2 'format!.*WORLD'

# Look for any other potential string conversions in auth.rs
rg -A 2 'to_string|format!' bin/sozo/src/commands/auth.rs

Length of output: 5787

crates/sozo/ops/src/migrate/mod.rs (1)

23-35: LGTM! Imports are well organized.

The new imports appropriately support the metadata upload functionality.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Outside diff range and nitpick comments (7)
bin/sozo/src/commands/options/ipfs.rs (1)

30-42: Ohayo! Add input validation for username and password

The implementation looks clean, but consider adding validation for:

  • Username format (e.g., no special characters)
  • Password strength requirements
  • URL accessibility check

Here's a suggested enhancement:

 pub fn config(&self) -> Option<IpfsConfig> {
     trace!("Retrieving IPFS config for IpfsOptions.");
 
     let url = self.ipfs_url.as_ref().map(|url| url.to_string());
-    let username = self.ipfs_username.clone();
-    let password = self.ipfs_password.clone();
+    let username = self.ipfs_username.clone()
+        .filter(|u| is_valid_username(u));
+    let password = self.ipfs_password.clone()
+        .filter(|p| is_strong_password(p));
 
     if let (Some(url), Some(username), Some(password)) = (url, username, password) {
         Some(IpfsConfig { url, username, password })
     } else {
         None
     }
 }
crates/dojo/world/src/metadata/metadata_storage.rs (1)

92-102: Handle partial upload failures gracefully

Ohayo, sensei! The current implementation might leave metadata in an inconsistent state if one of the uploads fails (e.g., icon uploaded but metadata upload fails).

Consider implementing a two-phase upload:

  1. Upload all assets first and collect URIs
  2. Only update metadata if all assets were uploaded successfully

Example approach:

async fn upload(&self, service: &mut impl UploadService) -> Result<String> {
    // Phase 1: Upload all assets
    let uploads = futures::future::try_join_all(vec![
        upload_uri(&self.icon_uri, service),
        upload_uri(&self.cover_uri, service),
    ]).await?;
    
    // Phase 2: Only if all assets uploaded successfully, update metadata
    let mut meta = self.clone();
    meta.icon_uri = uploads[0].clone();
    meta.cover_uri = uploads[1].clone();
    
    let serialized = json!(meta).to_string();
    service.upload(serialized.as_bytes().to_vec()).await
}

Also applies to: 107-115

crates/dojo/world/src/metadata/metadata_test.rs (2)

17-22: Ohayo sensei! Consider making the test file path handling more robust.

The test_file_path function could fail silently if the test data directory doesn't exist. Consider adding better error handling and validation.

 fn test_file_path(filename: &str) -> PathBuf {
-    fs::canonicalize(
-        PathBuf::from_str(&format!("./src/metadata/metadata_test_data/{}", filename)).unwrap(),
-    )
-    .unwrap()
+    let path = PathBuf::from_str(&format!("./src/metadata/metadata_test_data/{}", filename))
+        .expect("Failed to create path");
+    fs::canonicalize(&path)
+        .unwrap_or_else(|_| panic!("Test data file not found: {}", path.display()))
 }

72-76: Ohayo! Let's make the error handling more idiomatic, sensei.

The error handling pattern is repeated. Consider using Rust's Result combinators for more concise and idiomatic code.

-    let (current_uri, current_hash) = if let Ok(Some(res)) = res {
-        res
-    } else {
-        panic!("Upload failed");
-    };
+    let (current_uri, current_hash) = res
+        .expect("Upload failed")
+        .expect("No upload result");

Also applies to: 88-92

bin/sozo/src/commands/auth.rs (2)

681-681: Semantic handling of world resource in PermissionPair.

The use of WORLD constant in to_selector_and_address improves code clarity. However, consider adding a test case for invalid resource tags.

Consider adding this test case:

 #[test]
 fn test_permission_pair_to_selector_and_address() {
     // ... existing tests ...
+    
+    // Test invalid resource tag
+    let pair = PermissionPair {
+        resource_tag: "0xinvalid".to_string(),
+        grantee_tag_or_address: "0x456".to_string(),
+    };
+    assert!(pair.to_selector_and_address(&contracts).is_err());
 }

Line range hint 1-804: Ohayo sensei! Here's a high-level architectural suggestion.

The changes improve the handling of world resources by using a semantic constant. Consider documenting the significance of the WORLD constant and its usage patterns in the module-level documentation to help future maintainers.

Add this documentation at the top of the file:

//! Authentication and permission management for Dojo worlds.
//!
//! This module handles the management of permissions for resources in a Dojo world.
//! The special `WORLD` constant is used to represent world-level resources and permissions,
//! which are fundamental to the Dojo engine's security model.
crates/sozo/ops/src/migrate/mod.rs (1)

116-170: Ohayo sensei! Consider adding retry mechanism and progress tracking

The implementation efficiently handles metadata uploads with proper error handling and multicall optimization. However, consider these improvements:

  1. Add a retry mechanism for failed uploads
  2. Implement progress tracking for individual uploads

Consider adding retry logic:

 async fn upload_metadata(
     &self,
     ui: &mut MigrationUi,
     service: &mut impl UploadService,
 ) -> anyhow::Result<()> {
     ui.update_text("Uploading metadata...");
 
     let mut invoker = Invoker::new(&self.world.account, self.txn_config);
+    const MAX_RETRIES: u32 = 3;
+    const RETRY_DELAY: Duration = Duration::from_secs(1);
 
     // world
     let current_hash =
         self.diff.resources.get(&WORLD).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(service, current_hash).await?;
+    let res = retry::retry(retry::delay::Fixed::new(RETRY_DELAY)
+        .take(MAX_RETRIES), || async {
+            new_metadata.upload_if_changed(service, current_hash).await
+    }).await?;
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between cdf4b4a and 0e51fb9.

⛔ Files ignored due to path filters (5)
  • crates/torii/types-test/Scarb.lock is excluded by !**/*.lock
  • examples/simple/Scarb.lock is excluded by !**/*.lock
  • examples/spawn-and-move/Scarb.lock is excluded by !**/*.lock
  • spawn-and-move-db.tar.gz is excluded by !**/*.gz
  • types-test-db.tar.gz is excluded by !**/*.gz
📒 Files selected for processing (24)
  • bin/sozo/src/commands/auth.rs (5 hunks)
  • bin/sozo/src/commands/dev.rs (2 hunks)
  • bin/sozo/src/commands/migrate.rs (6 hunks)
  • bin/sozo/src/commands/options/ipfs.rs (1 hunks)
  • crates/dojo-world/Cargo.toml (1 hunks)
  • crates/dojo/world/Cargo.toml (1 hunks)
  • crates/dojo/world/src/config/environment.rs (2 hunks)
  • crates/dojo/world/src/config/ipfs_config.rs (1 hunks)
  • crates/dojo/world/src/config/mod.rs (1 hunks)
  • crates/dojo/world/src/config/profile_config.rs (6 hunks)
  • crates/dojo/world/src/constants.rs (1 hunks)
  • crates/dojo/world/src/lib.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_storage.rs (1 hunks)
  • crates/dojo/world/src/metadata/metadata_test.rs (1 hunks)
  • crates/dojo/world/src/metadata/mod.rs (1 hunks)
  • crates/dojo/world/src/remote/events_to_remote.rs (5 hunks)
  • crates/dojo/world/src/services/ipfs_service.rs (1 hunks)
  • crates/dojo/world/src/services/mock_upload_service.rs (1 hunks)
  • crates/dojo/world/src/services/mod.rs (1 hunks)
  • crates/dojo/world/src/services/upload_service.rs (1 hunks)
  • crates/sozo/ops/Cargo.toml (1 hunks)
  • crates/sozo/ops/src/migrate/mod.rs (2 hunks)
  • crates/sozo/ops/src/tests/migration.rs (4 hunks)
  • crates/torii/graphql/Cargo.toml (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (19)
  • bin/sozo/src/commands/dev.rs
  • bin/sozo/src/commands/migrate.rs
  • crates/dojo-world/Cargo.toml
  • crates/dojo/world/Cargo.toml
  • crates/dojo/world/src/config/environment.rs
  • crates/dojo/world/src/config/ipfs_config.rs
  • crates/dojo/world/src/config/mod.rs
  • crates/dojo/world/src/config/profile_config.rs
  • crates/dojo/world/src/constants.rs
  • crates/dojo/world/src/lib.rs
  • crates/dojo/world/src/metadata/mod.rs
  • crates/dojo/world/src/remote/events_to_remote.rs
  • crates/dojo/world/src/services/ipfs_service.rs
  • crates/dojo/world/src/services/mock_upload_service.rs
  • crates/dojo/world/src/services/mod.rs
  • crates/dojo/world/src/services/upload_service.rs
  • crates/sozo/ops/Cargo.toml
  • crates/sozo/ops/src/tests/migration.rs
  • crates/torii/graphql/Cargo.toml
🔇 Additional comments (10)
bin/sozo/src/commands/options/ipfs.rs (2)

1-116: Ohayo! Overall implementation looks solid with some security considerations

The implementation aligns well with the PR objectives for improving metadata handling. The code is well-structured with good test coverage. However, there are a few areas for improvement:

  1. Password security (critical)
  2. Input validation (good to have)
  3. Test environment cleanup (essential)

Once these are addressed, this will be a robust implementation, sensei!


22-26: ⚠️ Potential issue

Ohayo! Consider more secure password input methods

The current implementation exposes passwords via command-line arguments, which can be visible in process lists (ps aux). This security concern was previously raised and remains valid.

Consider implementing one of these alternatives:

  1. Interactive password prompt
  2. Password file
  3. Environment file
  4. Secret management service

Would you like me to provide an example implementation using a more secure method, sensei?

crates/dojo/world/src/metadata/metadata_storage.rs (1)

70-87: Verify change detection behavior

Ohayo, sensei! Let's verify that the change detection logic works as expected for the PR objectives.

✅ Verification successful

Change detection behavior is properly implemented

Ohayo, sensei! After analyzing the test implementation, I can confirm that the change detection behavior is working correctly. The tests in metadata_test.rs verify three key scenarios:

  • Initial upload with zero hash
  • No upload when hash matches
  • Re-upload when hash differs

The test coverage shows proper implementation of the hash-based change detection logic, with both positive and negative test cases.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Verify the implementation of metadata change detection

# Check for test coverage of change detection
echo "Checking test coverage for metadata change detection..."
rg -l "upload_if_changed.*current_hash" --type rust

# Look for potential test cases
echo "Looking for test cases..."
ast-grep --pattern 'fn $test_name() {
  $$$
  upload_if_changed($$$)
  $$$
}'

# Check for error handling in tests
echo "Checking error handling in tests..."
rg "assert.*Err.*upload_if_changed" --type rust

Length of output: 529


Script:

#!/bin/bash
# Let's examine the test implementation in metadata_test.rs
echo "Checking metadata test implementation..."
cat crates/dojo/world/src/metadata/metadata_test.rs

# Let's also check the usage in migrate/mod.rs
echo "Checking migrate implementation..."
cat crates/sozo/ops/src/migrate/mod.rs

Length of output: 38662

crates/dojo/world/src/metadata/metadata_test.rs (2)

139-194: Excellent test coverage, sensei! 🎯

The test thoroughly verifies:

  • Initial upload behavior
  • Hash-based change detection
  • Content integrity after upload
  • All metadata fields

1-194: Implementation aligns perfectly with PR objectives! 🎯

The tests effectively validate that:

  1. Metadata is uploaded only when changed (fixes World metadatas no uploaded after fail #2096)
  2. Re-uploads work correctly after failures
  3. Content integrity is maintained

This implementation should successfully resolve the issues with world metadata uploads.

bin/sozo/src/commands/auth.rs (4)

10-10: LGTM! Clean import of the WORLD constant.

The import of WORLD from dojo_world::constants improves code clarity by using a semantic constant instead of magic values.


421-428: Enhanced permission retrieval for world resources.

The use of WORLD constant in permission retrieval makes the code more explicit about its intent when dealing with world-level permissions.


792-792: LGTM! Comprehensive test coverage for world constant.

The test case properly verifies the world resource handling using the WORLD constant.


309-314: Improved world resource handling in clone_permissions.

The replacement of string formatting logic with a conditional check against WORLD constant enhances readability and maintainability. The consistent handling of world resources across the codebase reduces the chance of bugs.

Let's verify the consistent usage of the WORLD constant:

✅ Verification successful

Let me search for the actual usage of WORLD constant to understand its role better.


Ohayo sensei! After analyzing the codebase, I can now provide a clear verification of the review comment.

Consistent WORLD constant usage confirmed across the codebase

The codebase shows consistent usage of WORLD constant (defined as Felt::ZERO) in the world contract implementation. The conditional check against WORLD in the formatting logic is correctly aligned with the codebase's conventions:

  • The WORLD constant is properly defined in dojo_world::constants::WORLD as Felt::ZERO
  • The formatting pattern if r != &WORLD { format!("{:#066x}", r) } else { "World".to_string() } is consistent with how the world resource is handled throughout the codebase
  • This pattern is used in permission management, resource handling, and world contract interactions
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for any remaining Felt::ZERO usage that might need to be replaced with WORLD
rg "Felt::ZERO" -A 2 -B 2

Length of output: 69340


Script:

#!/bin/bash
# Search for WORLD constant usage
ast-grep --pattern 'const WORLD: Felt = Felt::ZERO'

# Search for WORLD constant references
rg "WORLD" -B 2 -A 2

Length of output: 41127

crates/sozo/ops/src/migrate/mod.rs (1)

23-23: LGTM! Well-organized imports

The new imports are properly organized and necessary for the metadata handling functionality.

Also applies to: 27-29, 33-33, 35-35

Comment on lines +63 to +73
fn options_read_from_env_variable() {
std::env::set_var(IPFS_URL_ENV_VAR, ENV_IPFS_URL);
std::env::set_var(IPFS_USERNAME_ENV_VAR, ENV_IPFS_USERNAME);
std::env::set_var(IPFS_PASSWORD_ENV_VAR, ENV_IPFS_PASSWORD);

let cmd = Command::parse_from([""]);
let config = cmd.options.config().unwrap();
assert_eq!(config.url, ENV_IPFS_URL.to_string());
assert_eq!(config.username, ENV_IPFS_USERNAME.to_string());
assert_eq!(config.password, ENV_IPFS_PASSWORD.to_string());
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Ohayo! Clean up environment variables after tests

The tests look comprehensive, but environment variables should be cleaned up after each test to prevent test interference.

 #[test]
 fn options_read_from_env_variable() {
     std::env::set_var(IPFS_URL_ENV_VAR, ENV_IPFS_URL);
     std::env::set_var(IPFS_USERNAME_ENV_VAR, ENV_IPFS_USERNAME);
     std::env::set_var(IPFS_PASSWORD_ENV_VAR, ENV_IPFS_PASSWORD);
 
+    // Use drop guard pattern for cleanup
+    struct Cleanup;
+    impl Drop for Cleanup {
+        fn drop(&mut self) {
+            std::env::remove_var(IPFS_URL_ENV_VAR);
+            std::env::remove_var(IPFS_USERNAME_ENV_VAR);
+            std::env::remove_var(IPFS_PASSWORD_ENV_VAR);
+        }
+    }
+    let _cleanup = Cleanup;
+
     let cmd = Command::parse_from([""]);
     let config = cmd.options.config().unwrap();
     assert_eq!(config.url, ENV_IPFS_URL.to_string());
     assert_eq!(config.username, ENV_IPFS_USERNAME.to_string());
     assert_eq!(config.password, ENV_IPFS_PASSWORD.to_string());
 }

Also applies to: 76-95

Comment on lines +78 to +84
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)));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve hash comparison reliability

Ohayo, sensei! The current hash comparison has potential issues:

  1. Using only the last 64 bits of the Felt field reduces collision resistance
  2. Zero-padding might mask important hash differences

Consider using the full hash space:

-        let new_hash = compute_metadata_hash(self);
-        let new_hash = Felt::from_raw([0, 0, 0, new_hash]);
+        let mut hasher = sha2::Sha256::new();
+        self.hash(&mut hasher);
+        let result = hasher.finalize();
+        let new_hash = Felt::from_bytes_be(&result)?;

Don't forget to add:

use sha2::Digest;

Comment on lines +39 to +47
async fn upload_uri(uri: &Option<Uri>, service: &mut impl UploadService) -> Result<Option<Uri>> {
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())
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance error handling with custom error types

Ohayo, sensei! Consider creating specific error types for better error handling and debugging:

+#[derive(Debug, thiserror::Error)]
+pub enum MetadataError {
+    #[error("Failed to read file: {0}")]
+    FileReadError(#[from] std::io::Error),
+    #[error("Failed to upload to IPFS: {0}")]
+    UploadError(String),
+}

 async fn upload_uri(uri: &Option<Uri>, service: &mut impl UploadService) -> Result<Option<Uri>> {
     if let Some(Uri::File(path)) = uri {
-        let data = std::fs::read(path)?;
+        let data = std::fs::read(path).map_err(MetadataError::FileReadError)?;
         let uploaded_uri = Uri::Ipfs(service.upload(data).await?);
         Ok(Some(uploaded_uri))
     } else {
         Ok(uri.clone())
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +172 to +199
async fn upload_metadata_from_resource_config(
&self,
service: &mut impl UploadService,
config: &[ResourceConfig],
) -> anyhow::Result<Vec<Call>> {
let mut calls = vec![];

for item in config {
let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag);

let current_hash =
self.diff.resources.get(&selector).map_or(Felt::ZERO, |r| r.metadata_hash());

let new_metadata = metadata_config::ResourceMetadata::from(item.clone());

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 {
resource_id: selector,
metadata_uri: ByteArray::from_string(&new_uri)?,
metadata_hash: new_hash,
}));
}
}

Ok(calls)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ohayo sensei! Handle selector computation error and optimize vector allocation

Two improvements needed:

  1. The selector computation error is not handled properly
  2. Vector allocation could be optimized

Apply these improvements:

 async fn upload_metadata_from_resource_config(
     &self,
     service: &mut impl UploadService,
     config: &[ResourceConfig],
 ) -> anyhow::Result<Vec<Call>> {
-    let mut calls = vec![];
+    let mut calls = Vec::with_capacity(config.len());
 
     for item in config {
-        let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag);
+        let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag)
+            .map_err(|e| anyhow!("Failed to compute selector for tag {}: {}", item.tag, e))?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async fn upload_metadata_from_resource_config(
&self,
service: &mut impl UploadService,
config: &[ResourceConfig],
) -> anyhow::Result<Vec<Call>> {
let mut calls = vec![];
for item in config {
let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag);
let current_hash =
self.diff.resources.get(&selector).map_or(Felt::ZERO, |r| r.metadata_hash());
let new_metadata = metadata_config::ResourceMetadata::from(item.clone());
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 {
resource_id: selector,
metadata_uri: ByteArray::from_string(&new_uri)?,
metadata_hash: new_hash,
}));
}
}
Ok(calls)
}
async fn upload_metadata_from_resource_config(
&self,
service: &mut impl UploadService,
config: &[ResourceConfig],
) -> anyhow::Result<Vec<Call>> {
let mut calls = Vec::with_capacity(config.len());
for item in config {
let selector = dojo_types::naming::compute_selector_from_tag_or_name(&item.tag)
.map_err(|e| anyhow!("Failed to compute selector for tag {}: {}", item.tag, e))?;
let current_hash =
self.diff.resources.get(&selector).map_or(Felt::ZERO, |r| r.metadata_hash());
let new_metadata = metadata_config::ResourceMetadata::from(item.clone());
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 {
resource_id: selector,
metadata_uri: ByteArray::from_string(&new_uri)?,
metadata_hash: new_hash,
}));
}
}
Ok(calls)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

World metadatas no uploaded after fail
2 participants