Skip to content

Commit

Permalink
Upload world metadata to ipfs
Browse files Browse the repository at this point in the history
  • Loading branch information
tarrencev committed Oct 10, 2023
1 parent c9b865b commit e6f3f74
Show file tree
Hide file tree
Showing 15 changed files with 554 additions and 162 deletions.
308 changes: 294 additions & 14 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions crates/dojo-world/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ camino.workspace = true
convert_case.workspace = true
dojo-types = { path = "../dojo-types" }
futures = "0.3.28"
ipfs-api-backend-hyper = { git = "https://github.com/ferristseng/rust-ipfs-api", rev = "af2c17f7b19ef5b9898f458d97a90055c3605633", features = [ "with-hyper-rustls" ] }
reqwest = { version = "0.11.18", default-features = false, features = [ "rustls-tls" ] }
scarb.workspace = true
serde.workspace = true
Expand Down
162 changes: 95 additions & 67 deletions crates/dojo-world/src/metadata.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,77 @@
use std::io::Cursor;
use std::path::PathBuf;

use anyhow::Result;
use ipfs_api_backend_hyper::{IpfsApi, IpfsClient, TryFromUri};
use scarb::core::{ManifestMetadata, Workspace};
use serde::Deserialize;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_json::json;
use url::Url;

#[cfg(test)]
#[path = "metadata_test.rs"]
mod test;

pub fn dojo_metadata_from_workspace(ws: &Workspace<'_>) -> Option<DojoMetadata> {
pub fn dojo_metadata_from_workspace(ws: &Workspace<'_>) -> Option<Metadata> {
Some(ws.current_package().ok()?.manifest.metadata.dojo())
}

#[derive(Default, Deserialize, Debug, Clone)]
pub struct DojoMetadata {
pub world: Option<World>,
pub struct Metadata {
pub world: Option<WorldMetadata>,
pub env: Option<Environment>,
}

#[derive(Default, Deserialize, Debug, Clone)]
pub struct World {
#[derive(Debug)]
pub enum UriParseError {
InvalidUri,
InvalidFileUri,
}

#[derive(Debug, Clone, PartialEq)]
pub enum Uri {
Http(Url),
Ipfs(String),
File(PathBuf),
}

impl Serialize for Uri {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Uri::Http(url) => serializer.serialize_str(url.as_ref()),
Uri::Ipfs(ipfs) => serializer.serialize_str(ipfs),
Uri::File(path) => serializer.serialize_str(&format!("file://{}", path.display())),
}
}
}

impl<'de> Deserialize<'de> for Uri {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
if s.starts_with("ipfs://") {
Ok(Uri::Ipfs(s))
} else if let Some(path) = s.strip_prefix("file://") {
Ok(Uri::File(PathBuf::from(&path)))
} else if let Ok(url) = Url::parse(&s) {
Ok(Uri::Http(url))
} else {
Err(serde::de::Error::custom("Invalid Uri"))
}
}
}

#[derive(Default, Serialize, Deserialize, Debug, Clone)]
pub struct WorldMetadata {
pub name: Option<String>,
pub description: Option<String>,
pub image: Option<String>,
pub cover_uri: Option<Uri>,
pub icon_uri: Option<Uri>,
}

#[derive(Default, Deserialize, Clone, Debug)]
Expand Down Expand Up @@ -55,92 +111,64 @@ impl Environment {
}
}

impl World {
impl WorldMetadata {
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}

pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
}

pub fn image(&self) -> Option<&str> {
self.image.as_deref()
impl WorldMetadata {
pub async fn upload(&self) -> Result<String> {
let mut meta = self.clone();
let client = IpfsClient::from_str("https://ipfs.infura.io:5001")?
.with_credentials("2EBrzr7ZASQZKH32sl2xWauXPSA", "12290b883db9138a8ae3363b6739d220");

if let Some(Uri::File(icon)) = &self.icon_uri {
let icon_data = std::fs::read(icon)?;
let reader = Cursor::new(icon_data);
let response = client.add(reader).await?;
meta.icon_uri = Some(Uri::Ipfs(format!("ipfs://{}", response.hash)))
};

if let Some(Uri::File(cover)) = &self.cover_uri {
let cover_data = std::fs::read(cover)?;
let reader = Cursor::new(cover_data);
let response = client.add(reader).await?;
meta.cover_uri = Some(Uri::Ipfs(format!("ipfs://{}", response.hash)))
};

let serialized = json!(meta).to_string();
let reader = Cursor::new(serialized);
let response = client.add(reader).await?;

Ok(response.hash)
}
}

impl DojoMetadata {
impl Metadata {
pub fn env(&self) -> Option<&Environment> {
self.env.as_ref()
}

pub fn world(&self) -> Option<&World> {
pub fn world(&self) -> Option<&WorldMetadata> {
self.world.as_ref()
}
}
trait MetadataExt {
fn dojo(&self) -> DojoMetadata;
fn dojo(&self) -> Metadata;
}

impl MetadataExt for ManifestMetadata {
fn dojo(&self) -> DojoMetadata {
fn dojo(&self) -> Metadata {
self.tool_metadata
.as_ref()
.and_then(|e| e.get("dojo"))
.cloned()
.map(|v| v.try_into::<DojoMetadata>().unwrap_or_default())
.map(|v| v.try_into::<Metadata>().unwrap_or_default())
.unwrap_or_default()
}
}

#[cfg(test)]
mod test {
use super::DojoMetadata;

#[test]
fn check_deserialization() {
let metadata: DojoMetadata = toml::from_str(
r#"
[env]
rpc_url = "http://localhost:5050/"
account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973"
private_key = "0x1800000000300000180000000000030000000000003006001800006600"
keystore_path = "test/"
keystore_password = "dojo"
world_address = "0x0248cacaeac64c45be0c19ee8727e0bb86623ca7fa3f0d431a6c55e200697e5a"
[world]
name = "example"
description = "example world"
image = "example.png"
"#,
)
.unwrap();

assert!(metadata.env.is_some());
let env = metadata.env.unwrap();

assert_eq!(env.rpc_url(), Some("http://localhost:5050/"));
assert_eq!(
env.account_address(),
Some("0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973")
);
assert_eq!(
env.private_key(),
Some("0x1800000000300000180000000000030000000000003006001800006600")
);
assert_eq!(env.keystore_path(), Some("test/"));
assert_eq!(env.keystore_password(), Some("dojo"));
assert_eq!(
env.world_address(),
Some("0x0248cacaeac64c45be0c19ee8727e0bb86623ca7fa3f0d431a6c55e200697e5a")
);

assert!(metadata.world.is_some());
let world = metadata.world.unwrap();

assert_eq!(world.name(), Some("example"));
assert_eq!(world.description(), Some("example world"));
assert_eq!(world.image(), Some("example.png"));
}
}
63 changes: 63 additions & 0 deletions crates/dojo-world/src/metadata_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use super::WorldMetadata;
use crate::metadata::{Metadata, Uri};

#[test]
fn check_metadata_deserialization() {
let metadata: Metadata = toml::from_str(
r#"
[env]
rpc_url = "http://localhost:5050/"
account_address = "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973"
private_key = "0x1800000000300000180000000000030000000000003006001800006600"
keystore_path = "test/"
keystore_password = "dojo"
world_address = "0x0248cacaeac64c45be0c19ee8727e0bb86623ca7fa3f0d431a6c55e200697e5a"
[world]
name = "example"
description = "example world"
cover_uri = "file://example_cover.png"
icon_uri = "file://example_icon.png"
"#,
)
.unwrap();

assert!(metadata.env.is_some());
let env = metadata.env.unwrap();

assert_eq!(env.rpc_url(), Some("http://localhost:5050/"));
assert_eq!(
env.account_address(),
Some("0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973")
);
assert_eq!(
env.private_key(),
Some("0x1800000000300000180000000000030000000000003006001800006600")
);
assert_eq!(env.keystore_path(), Some("test/"));
assert_eq!(env.keystore_password(), Some("dojo"));
assert_eq!(
env.world_address(),
Some("0x0248cacaeac64c45be0c19ee8727e0bb86623ca7fa3f0d431a6c55e200697e5a")
);

assert!(metadata.world.is_some());
let world = metadata.world.unwrap();

assert_eq!(world.name(), Some("example"));
assert_eq!(world.description(), Some("example world"));
assert_eq!(world.cover_uri, Some(Uri::File("example_cover.png".into())));
assert_eq!(world.icon_uri, Some(Uri::File("example_icon.png".into())));
}

#[tokio::test]
async fn world_metadata_hash_and_upload() {
let meta = WorldMetadata {
name: Some("Test World".to_string()),
description: Some("A world used for testing".to_string()),
cover_uri: Some(Uri::File("src/metadata_test_data/cover.png".into())),
icon_uri: None,
};

let _ = meta.upload().await.unwrap();
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 0 additions & 10 deletions crates/dojo-world/src/migration/strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,6 @@ where
seed.map(poseidon_hash_single).ok_or(anyhow!("Missing seed for World deployment."))?;

world.salt = salt;
world.contract_address = get_contract_address(
salt,
diff.world.local,
&[
executor.as_ref().unwrap().contract_address,
base.as_ref().unwrap().diff.local,
FieldElement::ZERO,
],
FieldElement::ZERO,
);
}

Ok(MigrationStrategy { world_address, world, executor, base, contracts, models })
Expand Down
22 changes: 5 additions & 17 deletions crates/sozo/src/commands/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ use cairo_lang_filesystem::db::{AsFilesGroupMut, FilesGroupEx, PrivRawFileConten
use cairo_lang_filesystem::ids::FileId;
use clap::Args;
use dojo_world::manifest::Manifest;
use dojo_world::metadata::{dojo_metadata_from_workspace, DojoMetadata};
use dojo_world::migration::world::WorldDiff;
use notify_debouncer_mini::notify::RecursiveMode;
use notify_debouncer_mini::{new_debouncer, DebouncedEvent, DebouncedEventKind};
Expand Down Expand Up @@ -76,7 +75,6 @@ struct DevContext<'a> {
pub db: RootDatabase,
pub unit: CompilationUnit,
pub ws: Workspace<'a>,
pub dojo_metadata: Option<DojoMetadata>,
}

fn load_context(config: &Config) -> Result<DevContext<'_>> {
Expand All @@ -90,8 +88,7 @@ fn load_context(config: &Config) -> Result<DevContext<'_>> {
// we have only 1 unit in projects
let unit = compilation_units.get(0).unwrap();
let db = build_scarb_root_database(unit, &ws).unwrap();
let dojo_metadata = dojo_metadata_from_workspace(&ws);
Ok(DevContext { db, unit: unit.clone(), ws, dojo_metadata })
Ok(DevContext { db, unit: unit.clone(), ws })
}

fn build(context: &mut DevContext<'_>) -> Result<()> {
Expand Down Expand Up @@ -132,16 +129,9 @@ where
if total_diffs == 0 {
return Ok((new_manifest, world_address));
}
match migration::apply_diff(
target_dir,
diff,
name.clone(),
world_address,
account,
config.ui(),
None,
)
.await

match migration::apply_diff(ws, target_dir, diff, name.clone(), world_address, account, None)
.await
{
Ok(address) => {
config
Expand Down Expand Up @@ -202,18 +192,16 @@ impl DevArgs {
let name = self.name.clone();
let mut previous_manifest: Option<Manifest> = Option::None;
let result = build(&mut context);
let env_metadata = context.dojo_metadata.as_ref().and_then(|e| e.env.clone());

let Some((mut world_address, account)) = context
.ws
.config()
.tokio_handle()
.block_on(migration::setup_env(
&context.ws,
self.account,
self.starknet,
self.world,
env_metadata.as_ref(),
config.ui(),
name.as_ref(),
))
.ok()
Expand Down
9 changes: 1 addition & 8 deletions crates/sozo/src/commands/migrate.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use anyhow::Result;
use clap::Args;
use dojo_world::metadata::dojo_metadata_from_workspace;
use scarb::core::Config;

use super::options::account::AccountOptions;
Expand Down Expand Up @@ -53,15 +52,9 @@ impl MigrateArgs {
scarb::ops::compile(packages, &ws)?;
}

let env_metadata = dojo_metadata_from_workspace(&ws).and_then(|inner| inner.env().cloned());
// TODO: Check the updated scarb way to read profile specific values

ws.config().tokio_handle().block_on(migration::execute(
self,
env_metadata,
target_dir,
ws.config().ui(),
))?;
ws.config().tokio_handle().block_on(migration::execute(&ws, self, target_dir))?;

Ok(())
}
Expand Down
Loading

0 comments on commit e6f3f74

Please sign in to comment.