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 18, 2024
1 parent e8a1bac commit 7c34cc3
Show file tree
Hide file tree
Showing 10 changed files with 235 additions and 177 deletions.
14 changes: 13 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,13 @@ 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.")?;
let mut metadata_service =
IpfsMetadataService::new(IPFS_CLIENT_URL, IPFS_USERNAME, IPFS_PASSWORD)?;

migration
.upload_metadata(&mut spinner, &mut metadata_service)
.await
.context("Metadata upload failed.")?;

spinner.update_text("Writing manifest...");
ws.write_manifest_profile(manifest).context("🪦 Failed to write manifest.")?;
Expand Down
30 changes: 30 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,30 @@
use std::collections::HashMap;
use std::hash::{DefaultHasher, Hash, Hasher};

use anyhow::Result;

use super::metadata_service::MetadataService;

#[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.

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

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

use super::metadata_service::MetadataService;

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 {
pub fn new(client_url: &str, username: &str, password: &str) -> Result<Self> {
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?;
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)
}
}
9 changes: 9 additions & 0 deletions crates/dojo/world/src/metadata/metadata_service.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use anyhow::Result;

#[allow(async_fn_in_trait)]
pub trait MetadataService: std::marker::Send + std::marker::Sync + std::marker::Unpin {
async fn upload(&mut self, data: Vec<u8>) -> Result<String>;

#[cfg(test)]
async fn get(&self, uri: String) -> Result<Vec<u8>>;
}
78 changes: 78 additions & 0 deletions crates/dojo/world/src/metadata/metadata_storage.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use std::hash::{DefaultHasher, Hash, Hasher};

use anyhow::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 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()
}

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())
}
}

#[allow(async_fn_in_trait)]
pub trait MetadataStorage {
async fn upload(&self, service: &mut impl MetadataService) -> Result<String>;

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?;
meta.cover_uri = upload_uri(&self.cover_uri, service).await?;

let serialized = json!(meta).to_string();
service.upload(serialized.as_bytes().to_vec()).await
}
}

#[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?;

let serialized = json!(meta).to_string();
service.upload(serialized.as_bytes().to_vec()).await
}
}
Loading

0 comments on commit 7c34cc3

Please sign in to comment.