Skip to content

Commit

Permalink
Support grant/revoke writer/owner (#1572)
Browse files Browse the repository at this point in the history
  • Loading branch information
tarrencev authored Feb 25, 2024
1 parent 5faf5b2 commit 82a5bb8
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 75 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions bin/sozo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ dojo-bindgen.workspace = true
dojo-lang.workspace = true
dojo-types.workspace = true
dojo-world = { workspace = true, features = [ "contracts", "metadata", "migration" ] }
futures.workspace = true
notify = "6.0.1"
notify-debouncer-mini = "0.3.0"
scarb-ui.workspace = true
Expand Down
174 changes: 170 additions & 4 deletions bin/sozo/src/commands/auth.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
use std::str::FromStr;

use anyhow::Result;
use clap::{Args, Subcommand};
use dojo_world::contracts::cairo_utils;
use dojo_world::metadata::dojo_metadata_from_workspace;
use scarb::core::Config;
use starknet_crypto::FieldElement;

use super::options::account::AccountOptions;
use super::options::starknet::StarknetOptions;
Expand All @@ -15,17 +19,128 @@ pub struct AuthArgs {
pub command: AuthCommand,
}

#[derive(Debug, Clone, PartialEq)]
pub struct ModelContract {
pub model: FieldElement,
pub contract: String,
}

impl FromStr for ModelContract {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split(',').collect();

let (model, contract) = match parts.as_slice() {
[model, contract] => (model, contract),
_ => anyhow::bail!(
"Model and contract address are expected to be comma separated: `sozo auth writer \
model_name,0x1234`"
),
};

let model = cairo_utils::str_to_felt(model)
.map_err(|_| anyhow::anyhow!("Invalid model name: {}", model))?;

Ok(ModelContract { model, contract: contract.to_string() })
}
}

#[derive(Debug, Clone, PartialEq)]
pub enum ResourceType {
Contract(String),
Model(FieldElement),
}

#[derive(Debug, Clone, PartialEq)]
pub struct OwnerResource {
pub resource: ResourceType,
pub owner: FieldElement,
}

impl FromStr for OwnerResource {
type Err = anyhow::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let parts: Vec<&str> = s.split(',').collect();

let (resource_part, owner_part) = match parts.as_slice() {
[resource, owner] => (*resource, *owner),
_ => anyhow::bail!(
"Owner and resource are expected to be comma separated: `sozo auth owner \
resource_type:resource_name,0x1234`"
),
};

let owner = FieldElement::from_hex_be(owner_part)
.map_err(|_| anyhow::anyhow!("Invalid owner address: {}", owner_part))?;

let resource_parts = resource_part.split_once(':');
let resource = match resource_parts {
Some(("contract", name)) => ResourceType::Contract(name.to_string()),
Some(("model", name)) => {
let model = cairo_utils::str_to_felt(name)
.map_err(|_| anyhow::anyhow!("Invalid model name: {}", name))?;
ResourceType::Model(model)
}
_ => anyhow::bail!(
"Resource is expected to be in the format `resource_type:resource_name`: `sozo \
auth owner 0x1234,resource_type:resource_name`"
),
};

Ok(OwnerResource { owner, resource })
}
}

#[derive(Debug, Subcommand)]
pub enum AuthCommand {
#[command(about = "Auth a system with the given calldata.")]
pub enum AuthKind {
#[command(about = "Grant a contract permission to write to a model.")]
Writer {
#[arg(num_args = 1..)]
#[arg(required = true)]
#[arg(value_name = "model,contract_address")]
#[arg(help = "A list of models and contract address to grant write access to. Comma \
separated values to indicate model name and contract address e.g. \
model_name,0x1234 model_name,0x1111 ")]
models_contracts: Vec<String>,
model_name,path::to::contract model_name,contract_address ")]
models_contracts: Vec<ModelContract>,
},
#[command(about = "Grant ownership of a resource.")]
Owner {
#[arg(num_args = 1..)]
#[arg(required = true)]
#[arg(value_name = "resource,owner_address")]
#[arg(help = "A list of owners and resources to grant ownership to. Comma separated \
values to indicate owner address and resouce e.g. \
contract:path::to::contract,0x1234 contract:contract_address,0x1111, \
model:model_name,0xbeef")]
owners_resources: Vec<OwnerResource>,
},
}

#[derive(Debug, Subcommand)]
pub enum AuthCommand {
#[command(about = "Grant an auth role.")]
Grant {
#[command(subcommand)]
kind: AuthKind,

#[command(flatten)]
world: WorldOptions,

#[command(flatten)]
starknet: StarknetOptions,

#[command(flatten)]
account: AccountOptions,

#[command(flatten)]
transaction: TransactionOptions,
},
#[command(about = "Revoke an auth role.")]
Revoke {
#[command(subcommand)]
kind: AuthKind,

#[command(flatten)]
world: WorldOptions,
Expand Down Expand Up @@ -54,3 +169,54 @@ impl AuthArgs {
config.tokio_handle().block_on(auth::execute(self.command, env_metadata))
}
}

#[cfg(test)]
mod tests {
use std::str::FromStr;

use starknet_crypto::FieldElement;

use super::*;

#[test]
fn test_owner_resource_from_str() {
// Test valid input
let input = "contract:path::to::contract,0x1234";
let expected_owner = FieldElement::from_hex_be("0x1234").unwrap();
let expected_resource = ResourceType::Contract("path::to::contract".to_string());
let expected = OwnerResource { owner: expected_owner, resource: expected_resource };
let result = OwnerResource::from_str(input).unwrap();
assert_eq!(result, expected);

// Test valid input with model
let input = "model:model_name,0x1234";
let expected_owner = FieldElement::from_hex_be("0x1234").unwrap();
let expected_model = cairo_utils::str_to_felt("model_name").unwrap();
let expected_resource = ResourceType::Model(expected_model);
let expected = OwnerResource { owner: expected_owner, resource: expected_resource };
let result = OwnerResource::from_str(input).unwrap();
assert_eq!(result, expected);

// Test invalid input
let input = "invalid_input";
let result = OwnerResource::from_str(input);
assert!(result.is_err());
}

#[test]
fn test_model_contract_from_str() {
// Test valid input
let input = "model_name,0x1234";
let expected_model = cairo_utils::str_to_felt("model_name").unwrap();
let expected_contract = "0x1234";
let expected =
ModelContract { model: expected_model, contract: expected_contract.to_string() };
let result = ModelContract::from_str(input).unwrap();
assert_eq!(result, expected);

// Test invalid input
let input = "invalid_input";
let result = ModelContract::from_str(input);
assert!(result.is_err());
}
}
4 changes: 4 additions & 0 deletions bin/sozo/src/commands/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use starknet::core::types::FieldElement;
use super::options::account::AccountOptions;
use super::options::starknet::StarknetOptions;
use super::options::transaction::TransactionOptions;
use super::options::world::WorldOptions;
use crate::ops::execute;

#[derive(Debug, Args)]
Expand All @@ -31,6 +32,9 @@ pub struct ExecuteArgs {
#[command(flatten)]
pub account: AccountOptions,

#[command(flatten)]
pub world: WorldOptions,

#[command(flatten)]
pub transaction: TransactionOptions,
}
Expand Down
94 changes: 57 additions & 37 deletions bin/sozo/src/ops/auth.rs
Original file line number Diff line number Diff line change
@@ -1,57 +1,77 @@
use anyhow::{Context, Result};
use dojo_world::contracts::cairo_utils;
use dojo_world::contracts::world::WorldContract;
use dojo_world::metadata::Environment;
use dojo_world::utils::TransactionWaiter;
use starknet::accounts::Account;
use starknet::core::types::FieldElement;

use crate::commands::auth::AuthCommand;
use super::get_contract_address;
use crate::commands::auth::{AuthCommand, AuthKind, ResourceType};

pub async fn execute(command: AuthCommand, env_metadata: Option<Environment>) -> Result<()> {
match command {
AuthCommand::Writer { models_contracts, world, starknet, account, transaction } => {
let world_address = world.address(env_metadata.as_ref())?;
let provider = starknet.provider(env_metadata.as_ref())?;
AuthCommand::Grant { kind, world, starknet, account, transaction } => match kind {
AuthKind::Writer { models_contracts } => {
let world_address = world.address(env_metadata.as_ref())?;
let provider = starknet.provider(env_metadata.as_ref())?;

let account = account.account(&provider, env_metadata.as_ref()).await?;
let world = WorldContract::new(world_address, &account);
let account = account.account(&provider, env_metadata.as_ref()).await?;
let world = WorldContract::new(world_address, &account);

let mut calls = vec![];
let mut calls = Vec::new();

for mc in models_contracts {
let parts: Vec<&str> = mc.split(',').collect();
for mc in models_contracts {
let contract = get_contract_address(&world, mc.contract).await?;
calls.push(world.grant_writer_getcall(&mc.model, &contract.into()));
}

let (model, contract_part) = match parts.as_slice() {
[model, contract] => (model.to_string(), *contract),
_ => anyhow::bail!(
"Model and contract address are expected to be comma separated: `sozo \
auth writer model_name,0x1234`"
),
};
let res = account
.execute(calls)
.send()
.await
.with_context(|| "Failed to send transaction")?;

let contract = FieldElement::from_hex_be(contract_part)
.map_err(|_| anyhow::anyhow!("Invalid contract address: {}", contract_part))?;

calls.push(
world
.grant_writer_getcall(&cairo_utils::str_to_felt(&model)?, &contract.into()),
);
if transaction.wait {
let receipt = TransactionWaiter::new(res.transaction_hash, &provider).await?;
println!("{}", serde_json::to_string_pretty(&receipt)?);
} else {
println!("Transaction hash: {:#x}", res.transaction_hash);
}
}
AuthKind::Owner { owners_resources } => {
let world_address = world.address(env_metadata.as_ref())?;
let provider = starknet.provider(env_metadata.as_ref())?;

let account = account.account(&provider, env_metadata.as_ref()).await?;
let world = WorldContract::new(world_address, &account);

let mut calls = Vec::new();

for or in owners_resources {
let resource = match &or.resource {
ResourceType::Model(name) => *name,
ResourceType::Contract(name_or_address) => {
get_contract_address(&world, name_or_address.clone()).await?
}
};

calls.push(world.grant_owner_getcall(&or.owner.into(), &resource));
}

let res = account
.execute(calls)
.send()
.await
.with_context(|| "Failed to send transaction")?;

let res = account
.execute(calls)
.send()
.await
.with_context(|| "Failed to send transaction")?;

if transaction.wait {
let receipt = TransactionWaiter::new(res.transaction_hash, &provider).await?;
println!("{}", serde_json::to_string_pretty(&receipt)?);
} else {
println!("Transaction hash: {:#x}", res.transaction_hash);
if transaction.wait {
let receipt = TransactionWaiter::new(res.transaction_hash, &provider).await?;
println!("{}", serde_json::to_string_pretty(&receipt)?);
} else {
println!("Transaction hash: {:#x}", res.transaction_hash);
}
}
}
},
_ => todo!(),
}

Ok(())
Expand Down
Loading

0 comments on commit 82a5bb8

Please sign in to comment.