Skip to content

Commit

Permalink
feat(sozo): upload world/models/contracts metadata only if changed (#…
Browse files Browse the repository at this point in the history
…2691)

* feat(sozo): upload world/resources metadata if they changed

* inject ipfs service dependency

* get ipfs credentials from env/command-line

* add --fix option to rust_fmt.sh

* after review

* fix: ensure no upload if no change on the world

* fix: fix warning message

---------

Co-authored-by: glihm <[email protected]>
  • Loading branch information
remybar and glihm authored Nov 28, 2024
1 parent edaa6a2 commit fe5c48e
Show file tree
Hide file tree
Showing 54 changed files with 1,286 additions and 312 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 21 additions & 21 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,28 +140,28 @@ auto_impl = "1.2.0"
base64 = "0.21.2"
bigdecimal = "0.4.1"
bytes = "1.6"
cairo-lang-compiler = "2.8.4"
cairo-lang-debug = "2.8.4"
cairo-lang-defs = "2.8.4"
cairo-lang-compiler = "=2.8.4"
cairo-lang-debug = "=2.8.4"
cairo-lang-defs = "=2.8.4"
cairo-lang-diagnostics = "2.7.0"
cairo-lang-filesystem = "2.8.4"
cairo-lang-formatter = "2.8.4"
cairo-lang-language-server = "2.8.4"
cairo-lang-lowering = "2.8.4"
cairo-lang-parser = "2.8.4"
cairo-lang-plugins = { version = "2.8.4", features = [ "testing" ] }
cairo-lang-project = "2.8.4"
cairo-lang-semantic = "2.8.4"
cairo-lang-sierra = "2.8.4"
cairo-lang-sierra-generator = "2.8.4"
cairo-lang-sierra-to-casm = "2.8.4"
cairo-lang-starknet = "2.8.4"
cairo-lang-starknet-classes = "2.8.4"
cairo-lang-syntax = "2.8.4"
cairo-lang-test-plugin = "2.8.4"
cairo-lang-test-runner = "2.8.4"
cairo-lang-test-utils = "2.8.4"
cairo-lang-utils = "2.8.4"
cairo-lang-filesystem = "=2.8.4"
cairo-lang-formatter = "=2.8.4"
cairo-lang-language-server = "=2.8.4"
cairo-lang-lowering = "=2.8.4"
cairo-lang-parser = "=2.8.4"
cairo-lang-plugins = { version = "=2.8.4", features = [ "testing" ] }
cairo-lang-project = "=2.8.4"
cairo-lang-semantic = "=2.8.4"
cairo-lang-sierra = "=2.8.4"
cairo-lang-sierra-generator = "=2.8.4"
cairo-lang-sierra-to-casm = "=2.8.4"
cairo-lang-starknet = "=2.8.4"
cairo-lang-starknet-classes = "=2.8.4"
cairo-lang-syntax = "=2.8.4"
cairo-lang-test-plugin = "=2.8.4"
cairo-lang-test-runner = "=2.8.4"
cairo-lang-test-utils = "=2.8.4"
cairo-lang-utils = "=2.8.4"
cairo-vm = "1.0.0-rc4"
camino = { version = "1.1.2", features = [ "serde1" ] }
chrono = { version = "0.4.24", features = [ "serde" ] }
Expand Down
13 changes: 7 additions & 6 deletions bin/sozo/src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use clap::{Args, Subcommand};
use colored::Colorize;
use dojo_utils::Invoker;
use dojo_world::config::ProfileConfig;
use dojo_world::constants::WORLD;
use dojo_world::contracts::{ContractInfo, WorldContract};
use dojo_world::diff::{DiffPermissions, WorldDiff};
use scarb::core::{Config, Workspace};
Expand Down Expand Up @@ -305,12 +306,12 @@ async fn clone_permissions(
writer_of.extend(
external_writer_of
.iter()
.map(|r| if r != &Felt::ZERO { format!("{:#066x}", r) } else { "World".to_string() }),
.map(|r| if r != &WORLD { format!("{:#066x}", r) } else { "World".to_string() }),
);
owner_of.extend(
external_owner_of
.iter()
.map(|r| if r != &Felt::ZERO { format!("{:#066x}", r) } else { "World".to_string() }),
.map(|r| if r != &WORLD { format!("{:#066x}", r) } else { "World".to_string() }),
);

// Sort the tags to have a deterministic output.
Expand Down Expand Up @@ -417,13 +418,13 @@ async fn list_permissions(

let mut world_writers = world_diff
.external_writers
.get(&Felt::ZERO)
.get(&WORLD)
.map(|writers| writers.iter().cloned().collect::<Vec<_>>())
.unwrap_or_default();

let mut world_owners = world_diff
.external_owners
.get(&Felt::ZERO)
.get(&WORLD)
.map(|owners| owners.iter().cloned().collect::<Vec<_>>())
.unwrap_or_default();

Expand Down Expand Up @@ -677,7 +678,7 @@ impl PermissionPair {
contracts: &HashMap<String, ContractInfo>,
) -> Result<(Felt, Felt)> {
let selector = if self.resource_tag == "world" {
Felt::ZERO
WORLD
} else if self.resource_tag.starts_with("0x") {
Felt::from_str(&self.resource_tag)
.map_err(|_| anyhow!("Invalid resource selector: {}", self.resource_tag))?
Expand Down Expand Up @@ -788,7 +789,7 @@ mod tests {
grantee_tag_or_address: "0x123".to_string(),
};
let (selector, address) = pair.to_selector_and_address(&contracts).unwrap();
assert_eq!(selector, Felt::ZERO);
assert_eq!(selector, WORLD);
assert_eq!(address, Felt::from_str("0x123").unwrap());

let pair = PermissionPair {
Expand Down
6 changes: 6 additions & 0 deletions bin/sozo/src/commands/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use super::options::account::AccountOptions;
use super::options::starknet::StarknetOptions;
use super::options::transaction::TransactionOptions;
use super::options::world::WorldOptions;
use crate::commands::options::ipfs::IpfsOptions;

#[derive(Debug, Args)]
pub struct DevArgs {
Expand Down Expand Up @@ -82,11 +83,16 @@ impl DevArgs {
build_args.clone().run(config)?;
info!("Initial build completed.");

// As this `dev` command is for development purpose only,
// allowing to watch for changes, compile and migrate them,
// there is no need for metadata uploading. That's why,
// `ipfs` is set to its default value meaning it is disabled.
let migrate_args = MigrateArgs {
world: self.world,
starknet: self.starknet,
account: self.account,
transaction: self.transaction,
ipfs: IpfsOptions::default(),
};

let _ = migrate_args.clone().run(config);
Expand Down
44 changes: 40 additions & 4 deletions bin/sozo/src/commands/migrate.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use anyhow::{Context, Result};
use clap::Args;
use colored::Colorize;
use colored::*;
use dojo_utils::{self, TxnConfig};
use dojo_world::contracts::WorldContract;
use dojo_world::services::IpfsService;
use scarb::core::{Config, Workspace};
use sozo_ops::migrate::{Migration, MigrationResult};
use sozo_ops::migration_ui::MigrationUi;
Expand All @@ -14,6 +15,7 @@ use tabled::{Table, Tabled};
use tracing::trace;

use super::options::account::AccountOptions;
use super::options::ipfs::IpfsOptions;
use super::options::starknet::StarknetOptions;
use super::options::transaction::TransactionOptions;
use super::options::world::WorldOptions;
Expand All @@ -32,6 +34,9 @@ pub struct MigrateArgs {

#[command(flatten)]
pub account: AccountOptions,

#[command(flatten)]
pub ipfs: IpfsOptions,
}

impl MigrateArgs {
Expand All @@ -43,7 +48,7 @@ impl MigrateArgs {
ws.profile_check()?;
ws.ensure_profile_artifacts()?;

let MigrateArgs { world, starknet, account, .. } = self;
let MigrateArgs { world, starknet, account, ipfs, .. } = self;

config.tokio_handle().block_on(async {
print_banner(&ws, &starknet).await?;
Expand All @@ -60,6 +65,7 @@ impl MigrateArgs {
.await?;

let world_address = world_diff.world_info.address;
let profile_config = ws.load_profile_config()?;

let mut txn_config: TxnConfig = self.transaction.try_into()?;
txn_config.wait = true;
Expand All @@ -75,15 +81,45 @@ impl MigrateArgs {
let MigrationResult { manifest, has_changes } =
migration.migrate(&mut spinner).await.context("Migration failed.")?;

let ipfs_config =
ipfs.config().or(profile_config.env.map(|env| env.ipfs_config).unwrap_or(None));

if let Some(config) = ipfs_config {
let mut metadata_service = IpfsService::new(config)?;

migration
.upload_metadata(&mut spinner, &mut metadata_service)
.await
.context("Metadata upload failed.")?;
} else {
println!();
println!(
"{}",
"IPFS credentials not found. Metadata upload skipped. To upload metadata, configure IPFS credentials in your profile config or environment variables: https://book.dojoengine.org/framework/world/metadata.".bright_yellow()
);
};

spinner.update_text("Writing manifest...");
ws.write_manifest_profile(manifest).context("🪦 Failed to write manifest.")?;

let colored_address = format!("{:#066x}", world_address).green();

let (symbol, end_text) = if has_changes {
("⛩️ ", format!("Migration successful with world at address {}", colored_address))
(
"⛩️ ",
format!(
"Migration successful with world at address {}",
colored_address
),
)
} else {
("🪨 ", format!("No changes for world at address {:#066x}", world_address))
(
"🪨 ",
format!(
"No changes for world at address {:#066x}",
world_address
),
)
};

spinner.stop_and_persist_boxed(symbol, end_text);
Expand Down
116 changes: 116 additions & 0 deletions bin/sozo/src/commands/options/ipfs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use clap::Args;
use dojo_utils::env::{IPFS_PASSWORD_ENV_VAR, IPFS_URL_ENV_VAR, IPFS_USERNAME_ENV_VAR};
use dojo_world::config::IpfsConfig;
use tracing::trace;
use url::Url;

#[derive(Debug, Default, Args, Clone)]
#[command(next_help_heading = "IPFS options")]
pub struct IpfsOptions {
#[arg(long, env = IPFS_URL_ENV_VAR)]
#[arg(value_name = "URL")]
#[arg(help = "The IPFS URL.")]
#[arg(global = true)]
pub ipfs_url: Option<Url>,

#[arg(long, env = IPFS_USERNAME_ENV_VAR)]
#[arg(value_name = "USERNAME")]
#[arg(help = "The IPFS username.")]
#[arg(global = true)]
pub ipfs_username: Option<String>,

#[arg(long, env = IPFS_PASSWORD_ENV_VAR)]
#[arg(value_name = "PASSWORD")]
#[arg(help = "The IPFS password.")]
#[arg(global = true)]
pub ipfs_password: Option<String>,
}

impl IpfsOptions {
pub fn config(&self) -> Option<IpfsConfig> {
trace!("Retrieving IPFS config for IpfsOptions.");

let url = self.ipfs_url.as_ref().map(|url| url.to_string());
let username = self.ipfs_username.clone();
let password = self.ipfs_password.clone();

if let (Some(url), Some(username), Some(password)) = (url, username, password) {
Some(IpfsConfig { url, username, password })
} else {
None
}
}
}

#[cfg(test)]
mod tests {
use clap::Parser;
use dojo_utils::env::{IPFS_PASSWORD_ENV_VAR, IPFS_URL_ENV_VAR, IPFS_USERNAME_ENV_VAR};

use super::IpfsOptions;

#[derive(clap::Parser)]
struct Command {
#[clap(flatten)]
options: IpfsOptions,
}

const ENV_IPFS_URL: &str = "http://ipfs.service/";
const ENV_IPFS_USERNAME: &str = "johndoe";
const ENV_IPFS_PASSWORD: &str = "123456";

#[test]
fn options_read_from_env_variable() {
std::env::set_var(IPFS_URL_ENV_VAR, ENV_IPFS_URL);
std::env::set_var(IPFS_USERNAME_ENV_VAR, ENV_IPFS_USERNAME);
std::env::set_var(IPFS_PASSWORD_ENV_VAR, ENV_IPFS_PASSWORD);

let cmd = Command::parse_from([""]);
let config = cmd.options.config().unwrap();
assert_eq!(config.url, ENV_IPFS_URL.to_string());
assert_eq!(config.username, ENV_IPFS_USERNAME.to_string());
assert_eq!(config.password, ENV_IPFS_PASSWORD.to_string());
}

#[test]
fn cli_args_override_env_variables() {
std::env::set_var(IPFS_URL_ENV_VAR, ENV_IPFS_URL);
let url = "http://different.url/";
let username = "bobsmith";
let password = "654321";

let cmd = Command::parse_from([
"sozo",
"--ipfs-url",
url,
"--ipfs-username",
username,
"--ipfs-password",
password,
]);
let config = cmd.options.config().unwrap();
assert_eq!(config.url, url);
assert_eq!(config.username, username);
assert_eq!(config.password, password);
}

#[test]
fn invalid_url_format() {
let cmd = Command::try_parse_from([
"sozo",
"--ipfs-url",
"invalid-url",
"--ipfs-username",
"bobsmith",
"--ipfs-password",
"654321",
]);
assert!(cmd.is_err());
}

#[test]
fn options_not_provided_in_env_variable() {
let cmd = Command::parse_from(["sozo"]);
assert!(cmd.options.config().is_none());
}
}
1 change: 1 addition & 0 deletions bin/sozo/src/commands/options/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod account;
pub mod ipfs;
pub mod signer;
pub mod starknet;
pub mod transaction;
Expand Down
Loading

0 comments on commit fe5c48e

Please sign in to comment.