diff --git a/Cargo.lock b/Cargo.lock index 221db839c3..05935d4d43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -294,6 +294,12 @@ dependencies = [ "rand", ] +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + [[package]] name = "arrayvec" version = "0.7.4" @@ -580,6 +586,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + [[package]] name = "base16ct" version = "0.2.0" @@ -1106,7 +1118,7 @@ dependencies = [ "serde", "smol_str", "thiserror", - "toml", + "toml 0.7.8", ] [[package]] @@ -1651,6 +1663,22 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "common-multipart-rfc7578" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baee326bc603965b0f26583e1ecd7c111c41b49bd92a344897476a352798869" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http", + "mime", + "mime_guess", + "rand", + "thiserror", +] + [[package]] name = "console" version = "0.15.7" @@ -1725,6 +1753,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + [[package]] name = "cpufeatures" version = "0.2.9" @@ -1983,6 +2020,26 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "data-encoding-macro" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c904b33cc60130e1aeea4956ab803d08a3f4a0ca82d64ed757afac3891f2bb99" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fdf3fce3ce863539ec1d7fd1b6dcc3c645663376b43ed376bbf887733e4f772" +dependencies = [ + "data-encoding", + "syn 1.0.109", +] + [[package]] name = "deno_task_shell" version = "0.13.2" @@ -2121,7 +2178,16 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", +] + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", ] [[package]] @@ -2130,7 +2196,7 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", ] [[package]] @@ -2143,6 +2209,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dirs-sys" version = "0.4.1" @@ -2274,7 +2351,7 @@ dependencies = [ "starknet", "thiserror", "tokio", - "toml", + "toml 0.7.8", "tracing", "url", ] @@ -2310,6 +2387,7 @@ dependencies = [ "dojo-test-utils", "dojo-types", "futures", + "ipfs-api-backend-hyper", "reqwest", "scarb", "serde", @@ -2320,7 +2398,7 @@ dependencies = [ "starknet-crypto 0.6.0", "thiserror", "tokio", - "toml", + "toml 0.7.8", "tracing", "url", ] @@ -2604,7 +2682,7 @@ dependencies = [ "serde", "serde_json", "syn 2.0.37", - "toml", + "toml 0.7.8", "walkdir", ] @@ -2760,7 +2838,7 @@ checksum = "de34e484e7ae3cab99fbfd013d6c5dc7f9013676a4e0e414d8b12e1213e8b3ba" dependencies = [ "cfg-if", "const-hex", - "dirs", + "dirs 5.0.1", "dunce", "ethers-core", "glob", @@ -4106,6 +4184,34 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-multipart-rfc7578" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb2cf73e96e9925f4bed948e763aa2901c2f1a3a5f713ee41917433ced6671" +dependencies = [ + "bytes", + "common-multipart-rfc7578", + "futures-core", + "http", + "hyper", +] + +[[package]] +name = "hyper-rustls" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +dependencies = [ + "http", + "hyper", + "log", + "rustls 0.20.9", + "rustls-native-certs", + "tokio", + "tokio-rustls 0.23.4", +] + [[package]] name = "hyper-rustls" version = "0.24.1" @@ -4368,6 +4474,47 @@ dependencies = [ "winapi", ] +[[package]] +name = "ipfs-api-backend-hyper" +version = "0.6.0" +source = "git+https://github.com/ferristseng/rust-ipfs-api?rev=af2c17f7b19ef5b9898f458d97a90055c3605633#af2c17f7b19ef5b9898f458d97a90055c3605633" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bytes", + "futures", + "http", + "hyper", + "hyper-multipart-rfc7578", + "hyper-rustls 0.23.2", + "ipfs-api-prelude", + "thiserror", +] + +[[package]] +name = "ipfs-api-prelude" +version = "0.6.0" +source = "git+https://github.com/ferristseng/rust-ipfs-api?rev=af2c17f7b19ef5b9898f458d97a90055c3605633#af2c17f7b19ef5b9898f458d97a90055c3605633" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "common-multipart-rfc7578", + "dirs 4.0.0", + "futures", + "http", + "multiaddr", + "multibase", + "serde", + "serde_json", + "serde_urlencoded", + "thiserror", + "tokio", + "tokio-util", + "tracing", + "walkdir", +] + [[package]] name = "ipnet" version = "2.8.0" @@ -4949,6 +5096,61 @@ dependencies = [ "version_check", ] +[[package]] +name = "multiaddr" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b36f567c7099511fa8612bbbb52dda2419ce0bdbacf31714e3a5ffdb766d3bd" +dependencies = [ + "arrayref", + "byteorder", + "data-encoding", + "log", + "multibase", + "multihash", + "percent-encoding", + "serde", + "static_assertions", + "unsigned-varint", + "url", +] + +[[package]] +name = "multibase" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +dependencies = [ + "base-x", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835d6ff01d610179fbce3de1694d007e500bf33a7f29689838941d6bf783ae40" +dependencies = [ + "core2", + "multihash-derive", + "unsigned-varint", +] + +[[package]] +name = "multihash-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6d4752e6230d8ef7adf7bd5d8c4b1f6561c1014c5ba9a37445ccefe18aa1db" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + [[package]] name = "multimap" version = "0.8.3" @@ -5222,6 +5424,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + [[package]] name = "option-ext" version = "0.2.0" @@ -5735,12 +5943,12 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.3.1" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ - "once_cell", - "toml_edit", + "thiserror", + "toml 0.5.11", ] [[package]] @@ -6090,7 +6298,7 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls", + "hyper-rustls 0.24.1", "ipnet", "js-sys", "log", @@ -6243,6 +6451,18 @@ dependencies = [ "sct", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.3" @@ -6415,7 +6635,7 @@ dependencies = [ "smol_str", "thiserror", "tokio", - "toml", + "toml 0.7.8", "toml_edit", "tracing", "tracing-log", @@ -6478,6 +6698,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "schannel" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "schemars" version = "0.8.15" @@ -6551,6 +6780,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.19" @@ -7398,7 +7650,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597e3a746727984cb7ea2487b6a40726cad0dbe86628e7d429aa6b8c4c153db4" dependencies = [ - "dirs", + "dirs 5.0.1", "fs2", "hex", "once_cell", @@ -7440,6 +7692,18 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "tap" version = "1.0.1" @@ -7702,6 +7966,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.7.8" @@ -7907,6 +8180,7 @@ dependencies = [ "lazy_static", "log", "once_cell", + "scarb", "scarb-ui", "serde", "serde_json", @@ -8371,6 +8645,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/crates/dojo-world/Cargo.toml b/crates/dojo-world/Cargo.toml index cb650c7c2e..37602d00cb 100644 --- a/crates/dojo-world/Cargo.toml +++ b/crates/dojo-world/Cargo.toml @@ -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 diff --git a/crates/dojo-world/src/metadata.rs b/crates/dojo-world/src/metadata.rs index 0198872f9a..8108f21ce4 100644 --- a/crates/dojo-world/src/metadata.rs +++ b/crates/dojo-world/src/metadata.rs @@ -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 { +pub fn dojo_metadata_from_workspace(ws: &Workspace<'_>) -> Option { Some(ws.current_package().ok()?.manifest.metadata.dojo()) } #[derive(Default, Deserialize, Debug, Clone)] -pub struct DojoMetadata { - pub world: Option, +pub struct Metadata { + pub world: Option, pub env: Option, } -#[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(&self, serializer: S) -> Result + 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(deserializer: D) -> Result + 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, pub description: Option, - pub image: Option, + pub cover_uri: Option, + pub icon_uri: Option, } #[derive(Default, Deserialize, Clone, Debug)] @@ -55,7 +111,7 @@ impl Environment { } } -impl World { +impl WorldMetadata { pub fn name(&self) -> Option<&str> { self.name.as_deref() } @@ -63,84 +119,56 @@ impl World { 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 { + 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::().unwrap_or_default()) + .map(|v| v.try_into::().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")); - } -} diff --git a/crates/dojo-world/src/metadata_test.rs b/crates/dojo-world/src/metadata_test.rs new file mode 100644 index 0000000000..8525fe8925 --- /dev/null +++ b/crates/dojo-world/src/metadata_test.rs @@ -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(); +} diff --git a/crates/dojo-world/src/metadata_test_data/cover.png b/crates/dojo-world/src/metadata_test_data/cover.png new file mode 100644 index 0000000000..2169b565d0 Binary files /dev/null and b/crates/dojo-world/src/metadata_test_data/cover.png differ diff --git a/crates/dojo-world/src/migration/strategy.rs b/crates/dojo-world/src/migration/strategy.rs index a8fdefc1d6..d13367d13f 100644 --- a/crates/dojo-world/src/migration/strategy.rs +++ b/crates/dojo-world/src/migration/strategy.rs @@ -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 }) diff --git a/crates/sozo/src/commands/dev.rs b/crates/sozo/src/commands/dev.rs index b55fb33878..5db6cf1421 100644 --- a/crates/sozo/src/commands/dev.rs +++ b/crates/sozo/src/commands/dev.rs @@ -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}; @@ -76,7 +75,6 @@ struct DevContext<'a> { pub db: RootDatabase, pub unit: CompilationUnit, pub ws: Workspace<'a>, - pub dojo_metadata: Option, } fn load_context(config: &Config) -> Result> { @@ -90,8 +88,7 @@ fn load_context(config: &Config) -> Result> { // 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<()> { @@ -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 @@ -202,18 +192,16 @@ impl DevArgs { let name = self.name.clone(); let mut previous_manifest: Option = 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() diff --git a/crates/sozo/src/commands/migrate.rs b/crates/sozo/src/commands/migrate.rs index b56ccf3a52..4e20cee5c2 100644 --- a/crates/sozo/src/commands/migrate.rs +++ b/crates/sozo/src/commands/migrate.rs @@ -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; @@ -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(()) } diff --git a/crates/sozo/src/ops/migration/migration_test.rs b/crates/sozo/src/ops/migration/migration_test.rs index b8f9a6acd4..b21c459bb6 100644 --- a/crates/sozo/src/ops/migration/migration_test.rs +++ b/crates/sozo/src/ops/migration/migration_test.rs @@ -1,4 +1,5 @@ use camino::Utf8PathBuf; +use dojo_test_utils::compiler::build_test_config; use dojo_test_utils::migration::prepare_migration; use dojo_test_utils::sequencer::{ get_default_test_starknet_config, SequencerConfig, StarknetConfig, TestSequencer, @@ -6,7 +7,7 @@ use dojo_test_utils::sequencer::{ use dojo_world::manifest::Manifest; use dojo_world::migration::strategy::prepare_for_migration; use dojo_world::migration::world::WorldDiff; -use scarb_ui::{OutputFormat, Ui, Verbosity}; +use scarb::ops; use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; use starknet::core::chain_id; use starknet::core::types::{BlockId, BlockTag}; @@ -20,8 +21,11 @@ use crate::ops::migration::execute_strategy; #[tokio::test(flavor = "multi_thread")] async fn migrate_with_auto_mine() { - let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); - let migration = prepare_migration("../../examples/ecs/target/dev".into()).unwrap(); + let config = build_test_config("../../examples/ecs/Scarb.toml").unwrap(); + let ws = ops::read_workspace(config.manifest_path(), &config) + .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); + + let mut migration = prepare_migration("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; @@ -29,15 +33,18 @@ async fn migrate_with_auto_mine() { let mut account = sequencer.account(); account.set_block_id(BlockId::Tag(BlockTag::Pending)); - execute_strategy(&migration, &account, &ui, None).await.unwrap(); + execute_strategy(&ws, &mut migration, &account, None).await.unwrap(); sequencer.stop().unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn migrate_with_block_time() { - let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); - let migration = prepare_migration("../../examples/ecs/target/dev".into()).unwrap(); + let config = build_test_config("../../examples/ecs/Scarb.toml").unwrap(); + let ws = ops::read_workspace(config.manifest_path(), &config) + .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); + + let mut migration = prepare_migration("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start( SequencerConfig { block_time: Some(1000), ..Default::default() }, @@ -48,14 +55,17 @@ async fn migrate_with_block_time() { let mut account = sequencer.account(); account.set_block_id(BlockId::Tag(BlockTag::Pending)); - execute_strategy(&migration, &account, &ui, None).await.unwrap(); + execute_strategy(&ws, &mut migration, &account, None).await.unwrap(); sequencer.stop().unwrap(); } #[tokio::test(flavor = "multi_thread")] async fn migrate_with_small_fee_multiplier_will_fail() { - let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); - let migration = prepare_migration("../../examples/ecs/target/dev".into()).unwrap(); + let config = build_test_config("../../examples/ecs/Scarb.toml").unwrap(); + let ws = ops::read_workspace(config.manifest_path(), &config) + .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); + + let mut migration = prepare_migration("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start( Default::default(), @@ -75,9 +85,9 @@ async fn migrate_with_small_fee_multiplier_will_fail() { assert!( execute_strategy( - &migration, + &ws, + &mut migration, &account, - &ui, Some(TransactionOptions { fee_estimate_multiplier: Some(0.2f64) }), ) .await @@ -98,7 +108,9 @@ fn migrate_world_without_seed_will_fail() { #[ignore] #[tokio::test] async fn migration_from_remote() { - let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); + let config = build_test_config("../../examples/ecs/Scarb.toml").unwrap(); + let ws = ops::read_workspace(config.manifest_path(), &config) + .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); let target_dir = Utf8PathBuf::from_path_buf("../../examples/ecs/target/dev".into()).unwrap(); let sequencer = @@ -114,13 +126,13 @@ async fn migration_from_remote() { ExecutionEncoding::Legacy, ); - let manifest = Manifest::load_from_path(target_dir.clone()).unwrap(); + let manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); let world = WorldDiff::compute(manifest, None); - let migration = + let mut migration = prepare_for_migration(None, Some(felt!("0x12345")), target_dir.clone(), world).unwrap(); - execute_strategy(&migration, &account, &ui, None).await.unwrap(); + execute_strategy(&ws, &mut migration, &account, None).await.unwrap(); let local_manifest = Manifest::load_from_path(target_dir.join("manifest.json")).unwrap(); let remote_manifest = Manifest::from_remote( diff --git a/crates/sozo/src/ops/migration/mod.rs b/crates/sozo/src/ops/migration/mod.rs index 0be15b4a89..e32ae84e88 100644 --- a/crates/sozo/src/ops/migration/mod.rs +++ b/crates/sozo/src/ops/migration/mod.rs @@ -2,7 +2,7 @@ use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; use dojo_world::manifest::{Manifest, ManifestError}; -use dojo_world::metadata::Environment; +use dojo_world::metadata::dojo_metadata_from_workspace; use dojo_world::migration::contract::ContractMigration; use dojo_world::migration::strategy::{prepare_for_migration, MigrationStrategy}; use dojo_world::migration::world::WorldDiff; @@ -10,12 +10,13 @@ use dojo_world::migration::{ Declarable, DeployOutput, Deployable, MigrationError, RegisterOutput, StateDiff, }; use dojo_world::utils::TransactionWaiter; +use scarb::core::Workspace; use scarb_ui::Ui; use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; use starknet::core::types::{ BlockId, BlockTag, FieldElement, InvokeTransactionResult, StarknetError, }; -use starknet::core::utils::cairo_short_string_to_felt; +use starknet::core::utils::{cairo_short_string_to_felt, get_contract_address}; use starknet::providers::jsonrpc::HttpTransport; use torii_client::contract::world::WorldContract; @@ -37,21 +38,16 @@ use crate::commands::options::starknet::StarknetOptions; use crate::commands::options::transaction::TransactionOptions; use crate::commands::options::world::WorldOptions; -pub async fn execute( - args: MigrateArgs, - env_metadata: Option, - target_dir: U, - ui: &Ui, -) -> Result<()> +pub async fn execute(ws: &Workspace<'_>, args: MigrateArgs, target_dir: U) -> Result<()> where U: AsRef, { + let ui = ws.config().ui(); let MigrateArgs { account, starknet, world, name, .. } = args; // Setup account for migration and fetch world address if it exists. - let (world_address, account) = - setup_env(account, starknet, world, env_metadata.as_ref(), ui, name.as_ref()).await?; + let (world_address, account) = setup_env(ws, account, starknet, world, name.as_ref()).await?; // Load local and remote World manifests. @@ -69,20 +65,21 @@ where ui.print("\n✨ No changes to be made. Remote World is already up to date!") } else { // Mirate according to the diff. - apply_diff(target_dir, diff, name, world_address, &account, ui, Some(args.transaction)) + apply_diff(ws, target_dir, diff, name, world_address, &account, Some(args.transaction)) .await?; } Ok(()) } +#[allow(clippy::too_many_arguments)] pub(crate) async fn apply_diff( + ws: &Workspace<'_>, target_dir: U, diff: WorldDiff, name: Option, world_address: Option, account: &SingleOwnerAccount, - ui: &Ui, txn_config: Option, ) -> Result where @@ -90,11 +87,12 @@ where P: Provider + Sync + Send + 'static, S: Signer + Sync + Send + 'static, { - let strategy = prepare_migration(target_dir, diff, name, world_address, ui)?; + let ui = ws.config().ui(); + let mut strategy = prepare_migration(target_dir, diff, name, world_address, ui)?; println!(" "); - let block_height = execute_strategy(&strategy, account, ui, txn_config) + let block_height = execute_strategy(ws, &mut strategy, account, txn_config) .await .map_err(|e| anyhow!(e)) .with_context(|| "Problem trying to migrate.")?; @@ -122,18 +120,21 @@ where } pub(crate) async fn setup_env( + ws: &Workspace<'_>, account: AccountOptions, starknet: StarknetOptions, world: WorldOptions, - env_metadata: Option<&Environment>, - ui: &Ui, name: Option<&String>, ) -> Result<(Option, SingleOwnerAccount, LocalWallet>)> { - let world_address = world.address(env_metadata).ok(); + let ui = ws.config().ui(); + let metadata = dojo_metadata_from_workspace(ws); + let env = metadata.as_ref().and_then(|inner| inner.env()); + + let world_address = world.address(env).ok(); let account = { - let provider = starknet.provider(env_metadata)?; - let mut account = account.account(provider, env_metadata).await?; + let provider = starknet.provider(env)?; + let mut account = account.account(provider, env).await?; account.set_block_id(BlockId::Tag(BlockTag::Pending)); let address = account.address(); @@ -240,20 +241,23 @@ where // returns the Some(block number) at which migration world is deployed, returns none if world was // not redeployed pub async fn execute_strategy( - strategy: &MigrationStrategy, + ws: &Workspace<'_>, + strategy: &mut MigrationStrategy, migrator: &SingleOwnerAccount, - ui: &Ui, txn_config: Option, ) -> Result> where P: Provider + Sync + Send + 'static, S: Signer + Sync + Send + 'static, { + let ui = ws.config().ui(); + match &strategy.executor { Some(executor) => { ui.print_header("# Executor"); deploy_contract(executor, "executor", vec![], migrator, ui, &txn_config).await?; + // There is no world migration, so it exists already. if strategy.world.is_none() { let addr = strategy.world_address()?; let InvokeTransactionResult { transaction_hash } = @@ -291,16 +295,43 @@ where None => {} }; - match &strategy.world { + match &mut strategy.world { Some(world) => { ui.print_header("# World"); - let calldata = vec![ + let metadata = dojo_metadata_from_workspace(ws); + + let metadata_uri = if let Some(meta) = metadata.as_ref().and_then(|inner| inner.world()) + { + let hash = meta.upload().await?; + let mut parts = format!("ipfs://{hash}") + .chars() + .collect::>() + .chunks(31) + .map(|chunk| { + let s: String = chunk.iter().collect(); + cairo_short_string_to_felt(&s).unwrap() + }) + .collect::>(); + + ui.print_sub(format!("Metadata uri: ipfs://{hash}")); + + parts.insert(0, parts.len().into()); + + parts + } else { + vec![FieldElement::ZERO] + }; + + let mut calldata = vec![ strategy.executor.as_ref().unwrap().contract_address, strategy.base.as_ref().unwrap().diff.local, - FieldElement::ZERO, ]; - deploy_contract(world, "world", calldata, migrator, ui, &txn_config).await?; + calldata.extend(metadata_uri); + deploy_contract(world, "world", calldata.clone(), migrator, ui, &txn_config).await?; + + world.contract_address = + get_contract_address(world.salt, world.diff.local, &calldata, FieldElement::ZERO); ui.print_sub(format!("Contract address: {:#x}", world.contract_address)); } diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index 930d4c550c..13874ac66f 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -38,4 +38,5 @@ tracing.workspace = true [dev-dependencies] camino.workspace = true dojo-test-utils = { path = "../../dojo-test-utils" } +scarb.workspace = true sozo = { path = "../../sozo" } diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index c734d0a35e..7b1677d669 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -1,9 +1,10 @@ +use dojo_test_utils::compiler::build_test_config; use dojo_test_utils::migration::prepare_migration; use dojo_test_utils::sequencer::{ get_default_test_starknet_config, SequencerConfig, TestSequencer, }; use dojo_world::migration::strategy::MigrationStrategy; -use scarb_ui::{OutputFormat, Ui, Verbosity}; +use scarb::ops; use sozo::ops::migration::execute_strategy; use sqlx::sqlite::SqlitePoolOptions; use starknet::core::types::{BlockId, BlockTag, Event, FieldElement}; @@ -21,14 +22,16 @@ pub async fn bootstrap_engine<'a>( world: &'a WorldContractReader<'a, JsonRpcClient>, db: &'a mut Sql, provider: &'a JsonRpcClient, - migration: &MigrationStrategy, + migration: &mut MigrationStrategy, sequencer: &TestSequencer, ) -> Result>, Box> { let mut account = sequencer.account(); account.set_block_id(BlockId::Tag(BlockTag::Pending)); - let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); - execute_strategy(migration, &account, &ui, None).await.unwrap(); + let config = build_test_config("../../../examples/ecs/Scarb.toml").unwrap(); + let ws = ops::read_workspace(config.manifest_path(), &config) + .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); + execute_strategy(&ws, migration, &account, None).await.unwrap(); let mut engine = Engine::new( world, @@ -56,14 +59,14 @@ async fn test_load_from_remote() { let pool = SqlitePoolOptions::new().max_connections(5).connect("sqlite::memory:").await.unwrap(); sqlx::migrate!("../migrations").run(&pool).await.unwrap(); - let migration = prepare_migration("../../../examples/ecs/target/dev".into()).unwrap(); + let mut migration = prepare_migration("../../../examples/ecs/target/dev".into()).unwrap(); let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; let provider = JsonRpcClient::new(HttpTransport::new(sequencer.url())); let world = WorldContractReader::new(migration.world_address().unwrap(), &provider); let mut db = Sql::new(pool.clone(), migration.world_address().unwrap()).await.unwrap(); - let _ = bootstrap_engine(&world, &mut db, &provider, &migration, &sequencer).await; + let _ = bootstrap_engine(&world, &mut db, &provider, &mut migration, &sequencer).await; let models = sqlx::query("SELECT * FROM models").fetch_all(&pool).await.unwrap(); assert_eq!(models.len(), 2); diff --git a/examples/ecs/Scarb.toml b/examples/ecs/Scarb.toml index f0ce236efc..ad6b5299d2 100644 --- a/examples/ecs/Scarb.toml +++ b/examples/ecs/Scarb.toml @@ -16,6 +16,8 @@ build-external-contracts = [] [tool.dojo.world] name = "example" description = "example world" +icon_path = "assets/icon.png" +cover_path = "assets/cover.png" [tool.dojo.env] rpc_url = "http://localhost:5050/" diff --git a/examples/ecs/assets/cover.png b/examples/ecs/assets/cover.png new file mode 100644 index 0000000000..8ac043f479 Binary files /dev/null and b/examples/ecs/assets/cover.png differ diff --git a/examples/ecs/assets/icon.png b/examples/ecs/assets/icon.png new file mode 100644 index 0000000000..2169b565d0 Binary files /dev/null and b/examples/ecs/assets/icon.png differ