Skip to content

Commit

Permalink
inject ipfs service dependency
Browse files Browse the repository at this point in the history
  • Loading branch information
remybar committed Nov 19, 2024
1 parent e8a1bac commit 864e272
Show file tree
Hide file tree
Showing 10 changed files with 328 additions and 177 deletions.
19 changes: 18 additions & 1 deletion bin/sozo/src/commands/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use clap::Args;
use colored::Colorize;
use dojo_utils::{self, TxnConfig};
use dojo_world::contracts::WorldContract;
use dojo_world::metadata::IpfsMetadataService;
use scarb::core::{Config, Workspace};
use sozo_ops::migrate::{Migration, MigrationResult};
use sozo_ops::migration_ui::MigrationUi;
Expand All @@ -19,6 +20,11 @@ use super::options::transaction::TransactionOptions;
use super::options::world::WorldOptions;
use crate::utils;

// TODO: to remove and to be read from environment variables
const IPFS_CLIENT_URL: &str = "https://ipfs.infura.io:5001";
const IPFS_USERNAME: &str = "2EBrzr7ZASQZKH32sl2xWauXPSA";
const IPFS_PASSWORD: &str = "12290b883db9138a8ae3363b6739d220";

#[derive(Debug, Clone, Args)]
pub struct MigrateArgs {
#[command(flatten)]
Expand Down Expand Up @@ -75,7 +81,18 @@ impl MigrateArgs {
let MigrationResult { manifest, has_changes } =
migration.migrate(&mut spinner).await.context("Migration failed.")?;

migration.upload_metadata(&mut spinner).await.context("Metadata upload failed.")?;
match IpfsMetadataService::new(IPFS_CLIENT_URL, IPFS_USERNAME, IPFS_PASSWORD) {
Ok(mut metadata_service) => {
migration
.upload_metadata(&mut spinner, &mut metadata_service)
.await
.context("Metadata upload failed.")?;
}
_ => {
// Unable to instanciate IPFS service so metadata upload is ignored.
// TODO: add a message.
}
};

spinner.update_text("Writing manifest...");
ws.write_manifest_profile(manifest).context("🪦 Failed to write manifest.")?;
Expand Down
34 changes: 34 additions & 0 deletions crates/dojo/world/src/metadata/fake_metadata_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};

use anyhow::Result;

use super::metadata_service::MetadataService;

/// Fake implementation of MetadataService to be used for tests only.
/// It just stores uri and data in a HashMap when `upload` is called,
/// and returns these data when `get` is called.
#[derive(Debug, Default)]
pub struct FakeMetadataService {
data: HashMap<String, Vec<u8>>,
}

#[allow(async_fn_in_trait)]
impl MetadataService for FakeMetadataService {
async fn upload(&mut self, data: Vec<u8>) -> Result<String> {
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
let hash = hasher.finish();

let uri = format!("ipfs://{:x}", hash);
self.data.insert(uri.clone(), data);

Ok(uri)
}

#[cfg(test)]
async fn get(&self, uri: String) -> Result<Vec<u8>> {
Ok(self.data.get(&uri).cloned().unwrap_or(Vec::<u8>::new()))
}
}
50 changes: 0 additions & 50 deletions crates/dojo/world/src/metadata/ipfs.rs

This file was deleted.

71 changes: 71 additions & 0 deletions crates/dojo/world/src/metadata/ipfs_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use std::io::Cursor;

use anyhow::Result;
#[cfg(test)]
use futures::TryStreamExt;
use ipfs_api_backend_hyper::{IpfsApi, TryFromUri};

use super::metadata_service::MetadataService;

/// IPFS implementation of MetadataService, allowing to
/// upload metadata to IPFS.
pub struct IpfsMetadataService {
client: ipfs_api_backend_hyper::IpfsClient,
}

// impl required by clippy
impl std::fmt::Debug for IpfsMetadataService {
fn fmt(&self, _f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
Ok(())
}
}

impl IpfsMetadataService {
/// Instanciate a new IPFS Metadata service with IPFS credentials.
///
/// # Arguments
/// * `client_url` - The IPFS client URL
/// * `username` - The IPFS username
/// * `password` - The IPFS password
///
/// # Returns
/// A new `IpfsMetadataService` is the IPFS client has been successfully
/// instanciated or a Anyhow error if not.
pub fn new(client_url: &str, username: &str, password: &str) -> Result<Self> {
if client_url.is_empty() || username.is_empty() || password.is_empty() {
anyhow::bail!("Invalid credentials: empty values not allowed");
}
if !client_url.starts_with("http://") && !client_url.starts_with("https://") {
anyhow::bail!("Invalid client URL: must start with http:// or https://");
}

Ok(Self {
client: ipfs_api_backend_hyper::IpfsClient::from_str(client_url)?
.with_credentials(username, password),
})
}
}

#[allow(async_fn_in_trait)]
impl MetadataService for IpfsMetadataService {
async fn upload(&mut self, data: Vec<u8>) -> Result<String> {
let reader = Cursor::new(data);
let response = self
.client
.add(reader)
.await
.map_err(|e| anyhow::anyhow!("Failed to upload to IPFS: {}", e))?;
Ok(format!("ipfs://{}", response.hash))
}

#[cfg(test)]
async fn get(&self, uri: String) -> Result<Vec<u8>> {
let res = self
.client
.cat(&uri.replace("ipfs://", ""))
.map_ok(|chunk| chunk.to_vec())
.try_concat()
.await?;
Ok(res)
}
}
26 changes: 26 additions & 0 deletions crates/dojo/world/src/metadata/metadata_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use anyhow::Result;

/// MetadataService trait to be implemented to upload
/// some metadata on a specific storage system.
#[allow(async_fn_in_trait)]
pub trait MetadataService: std::marker::Send + std::marker::Sync + std::marker::Unpin {
/// Upload some bytes (`data`) to the storage system,
/// and get back a string URI.
///
/// # Arguments
/// * `data` - bytes to upload
///
/// # Returns
/// A string URI or a Anyhow error.
async fn upload(&mut self, data: Vec<u8>) -> Result<String>;

/// Read stored bytes from a URI. (for tests only)
///
/// # Arguments
/// * `uri` - the URI of the data to read
///
/// # Returns
/// the read bytes or a Anyhow error.
#[cfg(test)]
async fn get(&self, uri: String) -> Result<Vec<u8>>;
}
116 changes: 116 additions & 0 deletions crates/dojo/world/src/metadata/metadata_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use std::hash::{DefaultHasher, Hash, Hasher};

use anyhow::{Context, Result};
use serde_json::json;
use starknet_crypto::Felt;

use super::metadata_service::MetadataService;
use crate::config::metadata_config::{ResourceMetadata, WorldMetadata};
use crate::uri::Uri;

/// Helper function to compute metadata hash.
///
/// # Arguments
/// * `data` - the data to hash.
///
/// # Returns
/// The hash value.
fn compute_metadata_hash<T>(data: T) -> u64
where
T: Hash,
{
let mut hasher = DefaultHasher::new();
data.hash(&mut hasher);
hasher.finish()
}

/// Helper function to process an optional URI.
///
/// If the URI is set and refer to a local asset, this asset
/// is then uploaded using the provided MetadataService.
/// In any other case, the URI is kept as it is.
///
/// # Arguments
/// * `uri` - The URI to process
/// * `service` - The metadata service to use to upload assets.
///
/// # Returns
/// The updated URI or a Anyhow error.
async fn upload_uri(uri: &Option<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?);
Ok(Some(uploaded_uri))
} else {
Ok(uri.clone())
}
}

/// Trait to be implemented by metadata structs to be
/// uploadable on a storage system.
#[allow(async_fn_in_trait)]
pub trait MetadataStorage {
/// Upload metadata using the provided service.
///
/// # Arguments
/// * `service` - service to use to upload metadata
///
/// # Returns
/// The uploaded metadata URI or a Anyhow error.
async fn upload(&self, service: &mut impl MetadataService) -> Result<String>;

/// Upload metadata using the provided service, only if it has changed.
///
/// # Arguments
/// * `service` - service to use to upload metadata
/// * `current_hash` - the hash of the previously uploaded metadata
///
/// # Returns
/// The uploaded metadata URI or a Anyhow error.
async fn upload_if_changed(
&self,
service: &mut impl MetadataService,
current_hash: Felt,
) -> Result<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]);

if new_hash != current_hash {
let new_uri = self.upload(service).await?;
return Ok(Some((new_uri, new_hash)));
}

Ok(None)
}
}

#[allow(async_fn_in_trait)]
impl MetadataStorage for WorldMetadata {
async fn upload(&self, service: &mut impl MetadataService) -> Result<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")
}
}

#[allow(async_fn_in_trait)]
impl MetadataStorage for ResourceMetadata {
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")?;

let serialized = json!(meta).to_string();
service.upload(serialized.as_bytes().to_vec()).await.context("Failed to upload metadata")
}
}
Loading

0 comments on commit 864e272

Please sign in to comment.