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 aeb376f
Show file tree
Hide file tree
Showing 8 changed files with 182 additions and 122 deletions.
7 changes: 6 additions & 1 deletion bin/sozo/src/commands/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,12 @@ 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()?;

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
36 changes: 36 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,36 @@
use anyhow::Result;
use std::{
collections::HashMap,
hash::{DefaultHasher, Hash, Hasher},
};

use super::metadata_service::MetadataService;

pub struct FakeMetadataService {
data: HashMap<String, Vec<u8>>,
}

impl FakeMetadataService {
pub fn new() -> Self {
Self { data: HashMap::new() }
}
}

#[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).map(|x| x.clone()).unwrap_or(Vec::<u8>::new()))
}
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,38 @@
use std::io::Cursor;

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

use super::metadata_service::MetadataService;

const IPFS_CLIENT_URL: &str = "https://ipfs.infura.io:5001";
const IPFS_USERNAME: &str = "2EBrzr7ZASQZKH32sl2xWauXPSA";
const IPFS_PASSWORD: &str = "12290b883db9138a8ae3363b6739d220";

pub struct IpfsClient {
pub struct IpfsMetadataService {
client: ipfs_api_backend_hyper::IpfsClient,
}

impl IpfsClient {
impl IpfsMetadataService {
pub fn new() -> Result<Self> {
Ok(Self {
client: ipfs_api_backend_hyper::IpfsClient::from_str(IPFS_CLIENT_URL)?
.with_credentials(IPFS_USERNAME, IPFS_PASSWORD),
})
}
}

/// Upload a `data` on IPFS and get a IPFS URI.
///
/// # Arguments
/// * `data`: the data to upload
///
/// # Returns
/// Result<String> - returns the IPFS URI or a Anyhow error.
pub(crate) async fn upload<T>(&self, data: T) -> Result<String>
where
T: AsRef<[u8]> + std::marker::Send + std::marker::Sync + std::marker::Unpin + 'static,
{
#[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)]
pub(crate) async fn get(&self, uri: String) -> Result<Vec<u8>> {
async fn get(&self, uri: String) -> Result<Vec<u8>> {
let res = self
.client
.cat(&uri.replace("ipfs://", ""))
Expand Down
78 changes: 78 additions & 0 deletions crates/dojo/world/src/metadata/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use std::hash::{DefaultHasher, Hash, Hasher};

use super::metadata_service::MetadataService;
use anyhow::Result;
use serde_json::json;
use starknet_crypto::Felt;

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

#[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();

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?));
};

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?));
};

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

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?));
};

let serialized = json!(meta).to_string();
service.upload(serialized.as_bytes().to_vec()).await
}
}
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>>;
}
43 changes: 24 additions & 19 deletions crates/dojo/world/src/metadata/metadata_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ use std::str::FromStr;
use starknet_crypto::Felt;
use url::Url;

use crate::metadata::ipfs::IpfsClient;
use crate::metadata::{MetadataStorage, ResourceMetadata, WorldMetadata};
use super::fake_metadata_service::FakeMetadataService;
use super::metadata::MetadataStorage;
use super::metadata_service::MetadataService;
use crate::config::metadata_config::{ResourceMetadata, WorldMetadata};
use crate::uri::Uri;

fn build_world_metadata() -> WorldMetadata {
Expand Down Expand Up @@ -55,20 +57,20 @@ fn assert_ipfs_uri(uri: &Option<Uri>) {
}
}

async fn assert_ipfs_content(uri: String, path: PathBuf) {
let ipfs_client = IpfsClient::new().expect("Ipfs client failed");
let ipfs_data = ipfs_client.get(uri).await.expect("read metadata failed");
async fn assert_ipfs_content(service: &FakeMetadataService, uri: String, path: PathBuf) {
let ipfs_data = service.get(uri).await.expect("read metadata failed");
let expected_data = std::fs::read(path).expect("read local data failed");

assert_eq!(ipfs_data, expected_data);
}

#[tokio::test]
async fn test_world_metadata() {
let mut metadata_service = FakeMetadataService::new();

let world_metadata = build_world_metadata();

// first metadata upload without existing hash.
let res = world_metadata.upload_if_changed(Felt::ZERO).await;
let res = world_metadata.upload_if_changed(&mut metadata_service, Felt::ZERO).await;

let (current_uri, current_hash) = if let Ok(Some(res)) = res {
res
Expand All @@ -77,13 +79,14 @@ async fn test_world_metadata() {
};

// no change => the upload is not done.
let res = world_metadata.upload_if_changed(current_hash).await;
let res = world_metadata.upload_if_changed(&mut metadata_service, current_hash).await;

assert!(res.is_ok());
assert!(res.unwrap().is_none());

// different hash => metadata are reuploaded.
let res = world_metadata.upload_if_changed(current_hash + Felt::ONE).await;
let res =
world_metadata.upload_if_changed(&mut metadata_service, current_hash + Felt::ONE).await;

let (new_uri, new_hash) = if let Ok(Some(res)) = res {
res
Expand All @@ -94,9 +97,8 @@ async fn test_world_metadata() {
assert_eq!(new_uri, current_uri);
assert_eq!(new_hash, current_hash);

// read back the metadata stored on IPFS to be sure it is correctly written
let ipfs_client = IpfsClient::new().expect("Ipfs client failed");
let read_metadata = ipfs_client.get(current_uri).await.expect("read metadata failed");
// read back the metadata from service to be sure it is correctly written
let read_metadata = metadata_service.get(current_uri).await.expect("read metadata failed");

let read_metadata = str::from_utf8(&read_metadata);
assert!(read_metadata.is_ok());
Expand All @@ -120,6 +122,7 @@ async fn test_world_metadata() {

assert_ipfs_uri(&read_metadata.cover_uri);
assert_ipfs_content(
&metadata_service,
read_metadata.cover_uri.unwrap().to_string(),
fs::canonicalize(PathBuf::from_str("./src/metadata/metadata_test_data/cover.png").unwrap())
.unwrap(),
Expand All @@ -128,36 +131,38 @@ async fn test_world_metadata() {

assert_ipfs_uri(&read_metadata.icon_uri);
assert_ipfs_content(
&metadata_service,
read_metadata.icon_uri.unwrap().to_string(),
fs::canonicalize(PathBuf::from_str("./src/metadata/metadata_test_data/icon.png").unwrap())
.unwrap(),
)
.await;

// TODO: would be nice to fake IpfsClient for tests
}

#[tokio::test]
async fn test_resource_metadata() {
let mut metadata_service = FakeMetadataService::new();

let resource_metadata = build_resource_metadata();

// first metadata upload without existing hash.
let res = resource_metadata.upload_if_changed(Felt::ZERO).await;
let res = resource_metadata.upload_if_changed(&mut metadata_service, Felt::ZERO).await;
assert!(res.is_ok());
let res = res.unwrap();

assert!(res.is_some());
let (current_uri, current_hash) = res.unwrap();

// no change => the upload is not done.
let res = resource_metadata.upload_if_changed(current_hash).await;
let res = resource_metadata.upload_if_changed(&mut metadata_service, current_hash).await;
assert!(res.is_ok());
let res = res.unwrap();

assert!(res.is_none());

// different hash => metadata are reuploaded.
let res = resource_metadata.upload_if_changed(current_hash + Felt::ONE).await;
let res =
resource_metadata.upload_if_changed(&mut metadata_service, current_hash + Felt::ONE).await;
assert!(res.is_ok());
let res = res.unwrap();

Expand All @@ -168,8 +173,7 @@ async fn test_resource_metadata() {
assert_eq!(new_hash, current_hash);

// read back the metadata stored on IPFS to be sure it is correctly written
let ipfs_client = IpfsClient::new().expect("Ipfs client failed");
let read_metadata = ipfs_client.get(current_uri).await.expect("read metadata failed");
let read_metadata = metadata_service.get(current_uri).await.expect("read metadata failed");

let read_metadata = str::from_utf8(&read_metadata);
assert!(read_metadata.is_ok());
Expand All @@ -184,6 +188,7 @@ async fn test_resource_metadata() {

assert_ipfs_uri(&read_metadata.icon_uri);
assert_ipfs_content(
&metadata_service,
read_metadata.icon_uri.unwrap().to_string(),
fs::canonicalize(PathBuf::from_str("./src/metadata/metadata_test_data/icon.png").unwrap())
.unwrap(),
Expand Down
Loading

0 comments on commit aeb376f

Please sign in to comment.