Skip to content

Commit

Permalink
Sozo call command (starkware-libs#1704)
Browse files Browse the repository at this point in the history
* typo

* sozo: add call command

add `sozo call` command to be able to directly call view functions without using
`starkli`.

* add sozo call tests

* fix fmt

* add block-id option to call command
  • Loading branch information
remybar authored Mar 29, 2024
1 parent 0b35400 commit defc01a
Show file tree
Hide file tree
Showing 12 changed files with 429 additions and 5 deletions.
3 changes: 3 additions & 0 deletions bin/sozo/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use tracing_log::AsTrace;

use crate::commands::auth::AuthArgs;
use crate::commands::build::BuildArgs;
use crate::commands::call::CallArgs;
use crate::commands::clean::CleanArgs;
use crate::commands::completions::CompletionsArgs;
use crate::commands::dev::DevArgs;
Expand Down Expand Up @@ -79,6 +80,8 @@ pub enum Commands {
Test(TestArgs),
#[command(about = "Execute a world's system")]
Execute(ExecuteArgs),
#[command(about = "Call a world's system")]
Call(CallArgs),
#[command(about = "Interact with a worlds models")]
Model(ModelArgs),
#[command(about = "Register new models")]
Expand Down
56 changes: 56 additions & 0 deletions bin/sozo/src/commands/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
use anyhow::Result;
use clap::Args;
use scarb::core::Config;
use starknet::core::types::FieldElement;

use super::options::starknet::StarknetOptions;
use super::options::world::WorldOptions;
use crate::utils;

#[derive(Debug, Args)]
#[command(about = "Call a system with the given calldata.")]
pub struct CallArgs {
#[arg(help = "The address or the fully qualified name of the contract to call.")]
pub contract: String,

#[arg(help = "The name of the entrypoint to call.")]
pub entrypoint: String,

#[arg(short, long)]
#[arg(value_delimiter = ',')]
#[arg(help = "The calldata to be passed to the entrypoint. Comma separated values e.g., \
0x12345,0x69420.")]
pub calldata: Vec<FieldElement>,

#[arg(short, long)]
#[arg(help = "The block ID (could be a hash, a number, 'pending' or 'latest')")]
pub block_id: Option<String>,

#[command(flatten)]
pub starknet: StarknetOptions,

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

impl CallArgs {
pub fn run(self, config: &Config) -> Result<()> {
let env_metadata = utils::load_metadata_from_config(config)?;

config.tokio_handle().block_on(async {
let world_reader =
utils::world_reader_from_env_metadata(self.world, self.starknet, &env_metadata)
.await
.unwrap();

sozo_ops::call::call(
world_reader,
self.contract,
self.entrypoint,
self.calldata,
self.block_id,
)
.await
})
}
}
2 changes: 1 addition & 1 deletion bin/sozo/src/commands/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub struct ExecuteArgs {

#[arg(short, long)]
#[arg(value_delimiter = ',')]
#[arg(help = "The calldata to be passed to the system. Comma seperated values e.g., \
#[arg(help = "The calldata to be passed to the system. Comma separated values e.g., \
0x12345,0x69420.")]
pub calldata: Vec<FieldElement>,

Expand Down
2 changes: 2 additions & 0 deletions bin/sozo/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::args::Commands;

pub(crate) mod auth;
pub(crate) mod build;
pub(crate) mod call;
pub(crate) mod clean;
pub(crate) mod completions;
pub(crate) mod dev;
Expand All @@ -27,6 +28,7 @@ pub fn run(command: Commands, config: &Config) -> Result<()> {
Commands::Dev(args) => args.run(config),
Commands::Auth(args) => args.run(config),
Commands::Execute(args) => args.run(config),
Commands::Call(args) => args.run(config),
Commands::Model(args) => args.run(config),
Commands::Register(args) => args.run(config),
Commands::Events(args) => args.run(config),
Expand Down
44 changes: 44 additions & 0 deletions bin/sozo/src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use anyhow::Error;
use dojo_world::contracts::world::WorldContract;
use dojo_world::contracts::WorldContractReader;
use dojo_world::metadata::{dojo_metadata_from_workspace, Environment};
use scarb::core::Config;
use starknet::accounts::SingleOwnerAccount;
Expand All @@ -11,6 +12,15 @@ use crate::commands::options::account::AccountOptions;
use crate::commands::options::starknet::StarknetOptions;
use crate::commands::options::world::WorldOptions;

/// Load metadata from the Scarb configuration.
///
/// # Arguments
///
/// * `config` - Scarb project configuration.
///
/// # Returns
///
/// A [`Environment`] on success.
pub fn load_metadata_from_config(config: &Config) -> Result<Option<Environment>, Error> {
let env_metadata = if config.manifest_path().exists() {
let ws = scarb::ops::read_workspace(config.manifest_path(), config)?;
Expand All @@ -23,6 +33,18 @@ pub fn load_metadata_from_config(config: &Config) -> Result<Option<Environment>,
Ok(env_metadata)
}

/// Build a world contract from the provided environment.
///
/// # Arguments
///
/// * `world` - The world options such as the world address,
/// * `account` - The account options,
/// * `starknet` - The Starknet options such as the RPC url,
/// * `env_metadata` - Optional environment coming from Scarb configuration.
///
/// # Returns
///
/// A [`WorldContract`] on success.
pub async fn world_from_env_metadata(
world: WorldOptions,
account: AccountOptions,
Expand All @@ -35,3 +57,25 @@ pub async fn world_from_env_metadata(
let account = account.account(provider, env_metadata.as_ref()).await?;
Ok(WorldContract::new(world_address, account))
}

/// Build a world contract reader from the provided environment.
///
/// # Arguments
///
/// * `world` - The world options such as the world address,
/// * `starknet` - The Starknet options such as the RPC url,
/// * `env_metadata` - Optional environment coming from Scarb configuration.
///
/// # Returns
///
/// A [`WorldContractReader`] on success.
pub async fn world_reader_from_env_metadata(
world: WorldOptions,
starknet: StarknetOptions,
env_metadata: &Option<Environment>,
) -> Result<WorldContractReader<JsonRpcClient<HttpTransport>>, Error> {
let world_address = world.address(env_metadata.as_ref())?;
let provider = starknet.provider(env_metadata.as_ref())?;

Ok(WorldContractReader::new(world_address, provider))
}
4 changes: 4 additions & 0 deletions crates/dojo-test-utils/src/sequencer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ impl TestSequencer {
)
}

pub fn provider(&self) -> JsonRpcClient<HttpTransport> {
JsonRpcClient::new(HttpTransport::new(self.url.clone()))
}

pub fn account_at_index(
&self,
index: usize,
Expand Down
39 changes: 39 additions & 0 deletions crates/sozo/ops/src/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use anyhow::{Context, Result};
use dojo_world::contracts::WorldContractReader;
use starknet::core::types::{BlockId, BlockTag, FieldElement, FunctionCall};
use starknet::core::utils::get_selector_from_name;
use starknet::providers::Provider;

use crate::utils::{get_contract_address_from_reader, parse_block_id};

pub async fn call<P: Provider + Sync + Send>(
world_reader: WorldContractReader<P>,
contract: String,
entrypoint: String,
calldata: Vec<FieldElement>,
block_id: Option<String>,
) -> Result<()> {
let contract_address = get_contract_address_from_reader(&world_reader, contract).await?;
let block_id = if let Some(block_id) = block_id {
parse_block_id(block_id)?
} else {
BlockId::Tag(BlockTag::Pending)
};

let output = world_reader
.provider()
.call(
FunctionCall {
contract_address,
entry_point_selector: get_selector_from_name(&entrypoint)?,
calldata,
},
block_id,
)
.await
.with_context(|| format!("Failed to call {entrypoint}"))?;

println!("[ {} ]", output.iter().map(|o| format!("0x{:x}", o)).collect::<Vec<_>>().join(" "));

Ok(())
}
1 change: 1 addition & 0 deletions crates/sozo/ops/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod auth;
pub mod call;
pub mod events;
pub mod execute;
pub mod migration;
Expand Down
147 changes: 147 additions & 0 deletions crates/sozo/ops/src/tests/call.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
use dojo_test_utils::sequencer::{
get_default_test_starknet_config, SequencerConfig, TestSequencer,
};
use dojo_world::contracts::WorldContractReader;
use starknet::accounts::SingleOwnerAccount;
use starknet::providers::jsonrpc::HttpTransport;
use starknet::providers::JsonRpcClient;
use starknet::signers::LocalWallet;
use starknet_crypto::FieldElement;

use super::setup;
use crate::{call, utils};

const CONTRACT_NAME: &str = "dojo_examples::actions::actions";
const ENTRYPOINT: &str = "tile_terrain";

#[tokio::test]
async fn call_with_bad_address() {
let sequencer =
TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await;

let world = setup::setup(&sequencer).await.unwrap();
let provider = sequencer.provider();
let world_reader = WorldContractReader::new(world.address, provider);

assert!(
call::call(
world_reader,
"0xBadCoffeeBadCode".to_string(),
ENTRYPOINT.to_string(),
vec![FieldElement::ZERO, FieldElement::ZERO],
None
)
.await
.is_err()
);
}

#[tokio::test]
async fn call_with_bad_name() {
let sequencer =
TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await;

let world = setup::setup(&sequencer).await.unwrap();
let provider = sequencer.provider();
let world_reader = WorldContractReader::new(world.address, provider);

assert!(
call::call(
world_reader,
"BadName".to_string(),
ENTRYPOINT.to_string(),
vec![FieldElement::ZERO, FieldElement::ZERO],
None
)
.await
.is_err()
);
}

#[tokio::test]
async fn call_with_bad_entrypoint() {
let sequencer =
TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await;

let world = setup::setup(&sequencer).await.unwrap();
let provider = sequencer.provider();
let world_reader = WorldContractReader::new(world.address, provider);

assert!(
call::call(
world_reader,
CONTRACT_NAME.to_string(),
"BadEntryPoint".to_string(),
vec![FieldElement::ZERO, FieldElement::ZERO],
None
)
.await
.is_err()
);
}

#[tokio::test]
async fn call_with_bad_calldata() {
let sequencer =
TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await;

let world = setup::setup(&sequencer).await.unwrap();
let provider = sequencer.provider();
let world_reader = WorldContractReader::new(world.address, provider);

assert!(
call::call(world_reader, CONTRACT_NAME.to_string(), ENTRYPOINT.to_string(), vec![], None)
.await
.is_err()
);
}

#[tokio::test]
async fn call_with_contract_name() {
let sequencer =
TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await;

let world = setup::setup(&sequencer).await.unwrap();
let provider = sequencer.provider();
let world_reader = WorldContractReader::new(world.address, provider);

assert!(
call::call(
world_reader,
CONTRACT_NAME.to_string(),
ENTRYPOINT.to_string(),
vec![FieldElement::ZERO, FieldElement::ZERO],
None,
)
.await
.is_ok()
);
}

#[tokio::test]
async fn call_with_contract_address() {
let sequencer =
TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await;

let world = setup::setup(&sequencer).await.unwrap();
let provider = sequencer.provider();
let world_reader = WorldContractReader::new(world.address, provider);

let contract_address = utils::get_contract_address::<
SingleOwnerAccount<JsonRpcClient<HttpTransport>, LocalWallet>,
>(&world, CONTRACT_NAME.to_string())
.await
.unwrap();

assert!(
call::call(
world_reader,
format!("{:#x}", contract_address),
ENTRYPOINT.to_string(),
vec![FieldElement::ZERO, FieldElement::ZERO],
None,
)
.await
.is_ok()
);
}
1 change: 1 addition & 0 deletions crates/sozo/ops/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod auth;
pub mod call;
pub mod setup;
pub mod utils;
Loading

0 comments on commit defc01a

Please sign in to comment.