Skip to content

Commit

Permalink
feat(sozo): add support for external contracts management (#2995)
Browse files Browse the repository at this point in the history
* first version of external contract management

* improve the way we get the contract name from Event enum in the ABI

* fix rust fmt

* some small refactoring

* update policies

* rebuild test artifacts

* after review

* fmt + rebuild artifacts

* small refactor proposed by CodeRabbit

* profile config: check instance name format

* update sozo inspect to support external contracts

* update inspect world after review

* add world and external contracts support for sozo execute and sozo call

* add ERC20, ERC721 and ERC1155 contracts

* typo

* simplify mint functions

* remove dependency on dojo-utils for dojo-world

---------

Co-authored-by: glihm <[email protected]>
  • Loading branch information
remybar and glihm authored Feb 11, 2025
1 parent 32d5073 commit f0c1e0b
Show file tree
Hide file tree
Showing 36 changed files with 3,790 additions and 145 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ pretty_assertions = "1.2.1"
rand = "0.8.5"
rayon = "1.8.0"
regex = "1.10.3"
reqwest = { version = "0.11.27", features = [ "blocking", "json", "rustls-tls" ], default-features = false }
reqwest = { version = "0.11.27", features = [ "json", "rustls-tls" ], default-features = false }
rpassword = "7.2.0"
rstest = "0.18.2"
rstest_reuse = "0.6.0"
Expand Down
2 changes: 1 addition & 1 deletion bin/sozo/src/commands/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,7 @@ mod tests {
contracts.insert(
"actions".to_string(),
ContractInfo {
tag: "actions".to_string(),
tag_or_name: "actions".to_string(),
address: Felt::from_str("0x456").unwrap(),
entrypoints: vec![],
},
Expand Down
60 changes: 41 additions & 19 deletions bin/sozo/src/commands/call.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::HashMap;

use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Result};
use clap::Args;
use dojo_world::config::calldata_decoder;
use dojo_world::contracts::ContractInfo;
Expand All @@ -19,7 +19,9 @@ use crate::utils::{self, CALLDATA_DOC};
#[derive(Debug, Args)]
#[command(about = "Call a system with the given calldata.")]
pub struct CallArgs {
#[arg(help = "The tag or address of the contract to call.")]
#[arg(help = "* The tag or address of the Dojo contract to call OR,
* The address or the instance name of the Starknet contract to call OR,
* 'world' to call the Dojo world.")]
pub tag_or_address: ResourceDescriptor,

#[arg(help = "The name of the entrypoint to call.")]
Expand Down Expand Up @@ -54,37 +56,57 @@ impl CallArgs {

let profile_config = ws.load_profile_config()?;

let descriptor = self.tag_or_address.ensure_namespace(&profile_config.namespace.default);
let CallArgs { tag_or_address, .. } = self;

config.tokio_handle().block_on(async {
let descriptor =
tag_or_address.clone().ensure_namespace(&profile_config.namespace.default);

let local_manifest = ws.read_manifest_profile()?;

let calldata = calldata_decoder::decode_calldata(&self.calldata)?;

let contract_address = match &descriptor {
let contracts: HashMap<String, ContractInfo> = if self.diff || local_manifest.is_none()
{
let (world_diff, _, _) =
utils::get_world_diff_and_provider(self.starknet.clone(), self.world, &ws)
.await?;

(&world_diff).into()
} else {
match &local_manifest {
Some(manifest) => manifest.into(),
_ => bail!(
"Unable to get the list of contracts, either from the world or from the \
local manifest."
),
}
};

let mut contract_address = match &descriptor {
ResourceDescriptor::Address(address) => Some(*address),
ResourceDescriptor::Tag(tag) => {
let contracts: HashMap<String, ContractInfo> =
if self.diff || local_manifest.is_none() {
let (world_diff, _, _) = utils::get_world_diff_and_provider(
self.starknet.clone(),
self.world,
&ws,
)
.await?;

(&world_diff).into()
} else {
(&local_manifest.unwrap()).into()
};

// Try to find the contract to call among Dojo contracts
contracts.get(tag).map(|c| c.address)
}
ResourceDescriptor::Name(_) => {
unimplemented!("Expected to be a resolved tag with default namespace.")
}
};

if contract_address.is_none() {
contract_address = match &tag_or_address {
ResourceDescriptor::Name(name) => contracts.get(name).map(|c| c.address),
ResourceDescriptor::Address(_) | ResourceDescriptor::Tag(_) => {
// A contract should have already been found while searching for a Dojo
// contract.
None
}
}
}
.ok_or_else(|| anyhow!("Contract {descriptor} not found in the world diff."))?;

let contract_address = contract_address
.ok_or_else(|| anyhow!("Contract {descriptor} not found in the world diff."))?;

let block_id = if let Some(block_id) = self.block_id {
dojo_utils::parse_block_id(block_id)?
Expand Down
62 changes: 40 additions & 22 deletions bin/sozo/src/commands/execute.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, bail, Result};
use clap::Args;
use dojo_utils::{Invoker, TxnConfig};
use dojo_world::config::calldata_decoder;
Expand Down Expand Up @@ -26,8 +26,10 @@ pub struct ExecuteArgs {
A call is made up of a <TAG_OR_ADDRESS>, an <ENTRYPOINT> and an optional <CALLDATA>:
- <TAG_OR_ADDRESS>: the address or the tag (ex: dojo_examples-actions) of the contract to be \
called,
- <TAG_OR_ADDRESS>:
* the address or the tag of a Dojo contract (ex: dojo_examples-actions) to be called OR
* the address or the instance name of a Starknet contract (ex: WoodToken) to be called OR
* 'world' to call the Dojo world.
- <ENTRYPOINT>: the name of the entry point to be called,
Expand Down Expand Up @@ -99,27 +101,43 @@ impl ExecuteArgs {

while let Some(arg) = arg_iter.next() {
let tag_or_address = arg;
let descriptor = ResourceDescriptor::from_string(&tag_or_address)?
.ensure_namespace(&profile_config.namespace.default);

let contract_address = match &descriptor {
ResourceDescriptor::Address(address) => Some(*address),
ResourceDescriptor::Tag(tag) => contracts.get(tag).map(|c| c.address),
ResourceDescriptor::Name(_) => {
unimplemented!("Expected to be a resolved tag with default namespace.")

let contract_address = if tag_or_address == "world" {
match contracts.get(&tag_or_address) {
Some(c) => c.address,
None => bail!("Unable to find the world address."),
}
};
} else {
// first, try to find the contract to call among Dojo contracts
let descriptor = ResourceDescriptor::from_string(&tag_or_address)?
.ensure_namespace(&profile_config.namespace.default);

let mut contract_address = match &descriptor {
ResourceDescriptor::Address(address) => Some(*address),
ResourceDescriptor::Tag(tag) => contracts.get(tag).map(|c| c.address),
ResourceDescriptor::Name(_) => {
unimplemented!("Expected to be a resolved tag with default namespace.")
}
};

let contract_address = contract_address.ok_or_else(|| {
let mut message = format!("Contract {descriptor} not found in the manifest.");
if self.diff {
message.push_str(
" Run the command again with `--diff` to force the fetch of data from \
the chain.",
);
// if not found, try to find a Starknet contract matching with the provided
// contract name.
if contract_address.is_none() {
contract_address = contracts.get(&tag_or_address).map(|c| c.address);
}
anyhow!(message)
})?;

contract_address.ok_or_else(|| {
let mut message =
format!("Contract {descriptor} not found in the manifest.");
if self.diff {
message.push_str(
" Run the command again with `--diff` to force the fetch of data \
from the chain.",
);
}
anyhow!(message)
})?
};

let entrypoint = arg_iter.next().ok_or_else(|| {
anyhow!(
Expand All @@ -138,7 +156,7 @@ impl ExecuteArgs {
}

trace!(
contract=?descriptor,
contract=?contract_address,
entrypoint=entrypoint,
calldata=?calldata,
"Decoded call."
Expand Down
97 changes: 78 additions & 19 deletions bin/sozo/src/commands/inspect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use anyhow::Result;
use clap::Args;
use colored::*;
use dojo_types::naming;
use dojo_world::diff::{ResourceDiff, WorldDiff, WorldStatus};
use dojo_world::diff::{ExternalContractDiff, ResourceDiff, WorldDiff, WorldStatus};
use dojo_world::ResourceType;
use scarb::core::Config;
use serde::Serialize;
Expand All @@ -17,9 +17,9 @@ use crate::utils;

#[derive(Debug, Args)]
pub struct InspectArgs {
#[arg(help = "The tag of the resource to inspect. If not provided, a world summary will be \
displayed.")]
resource: Option<String>,
#[arg(help = "The tag of the resource or the external contract instance name to inspect. If \
not provided, a world summary will be displayed.")]
element: Option<String>,

#[command(flatten)]
world: WorldOptions,
Expand All @@ -33,14 +33,14 @@ impl InspectArgs {
trace!(args = ?self);
let ws = scarb::ops::read_workspace(config.manifest_path(), config)?;

let InspectArgs { world, starknet, resource } = self;
let InspectArgs { world, starknet, element } = self;

config.tokio_handle().block_on(async {
let (world_diff, _, _) =
utils::get_world_diff_and_provider(starknet.clone(), world, &ws).await?;

if let Some(resource) = resource {
inspect_resource(&resource, &world_diff)?;
if let Some(element) = element {
inspect_element(&element, &world_diff)?;
} else {
inspect_world(&world_diff);
}
Expand Down Expand Up @@ -135,6 +135,24 @@ struct EventInspect {
selector: String,
}

#[derive(Debug, Tabled, Serialize)]
struct ExternalContractInspect {
#[tabled(rename = "External Contract")]
contract_name: String,
#[tabled(rename = "Instance Name")]
instance_name: String,
#[tabled(skip)]
class_hash: String,
#[tabled(rename = "Status")]
status: ResourceStatus,
#[tabled(skip)]
salt: String,
#[tabled(skip)]
constructor_calldata: Vec<String>,
#[tabled(rename = "Contract Address")]
address: String,
}

#[derive(Debug, Tabled)]
enum GranteeSource {
#[tabled(rename = "Local")]
Expand Down Expand Up @@ -165,21 +183,25 @@ struct GranteeDisplay {
source: GranteeSource,
}

/// Inspects a resource.
fn inspect_resource(resource_name_or_tag: &str, world_diff: &WorldDiff) -> Result<()> {
let selector = if naming::is_valid_tag(resource_name_or_tag) {
naming::compute_selector_from_tag(resource_name_or_tag)
/// Inspects a world element (resource or external contract).
fn inspect_element(element_name: &str, world_diff: &WorldDiff) -> Result<()> {
let selector = if naming::is_valid_tag(element_name) {
naming::compute_selector_from_tag(element_name)
} else {
naming::compute_bytearray_hash(resource_name_or_tag)
naming::compute_bytearray_hash(element_name)
};
let resource_diff = world_diff.resources.get(&selector);

if resource_diff.is_none() {
return Err(anyhow::anyhow!("Resource not found locally."));
if let Some(diff) = world_diff.resources.get(&selector) {
inspect_resource(diff, world_diff)
} else if let Some(diff) = world_diff.external_contracts.get(element_name) {
inspect_external_contract(diff)
} else {
Err(anyhow::anyhow!("Resource or external contract not found locally."))
}
}

let resource_diff = resource_diff.unwrap();

/// Inspects a resource.
fn inspect_resource(resource_diff: &ResourceDiff, world_diff: &WorldDiff) -> Result<()> {
let inspect = resource_diff_display(world_diff, resource_diff);
pretty_print_toml(&toml::to_string_pretty(&inspect).unwrap());

Expand Down Expand Up @@ -245,6 +267,14 @@ fn inspect_resource(resource_name_or_tag: &str, world_diff: &WorldDiff) -> Resul
Ok(())
}

/// Inspects an external contract.
fn inspect_external_contract(contract_diff: &ExternalContractDiff) -> Result<()> {
let inspect = external_contract_diff_display(contract_diff);
print_section_header("[External Contract]");
pretty_print_toml(&toml::to_string_pretty(&inspect).unwrap());
Ok(())
}

/// Inspects the whole world.
fn inspect_world(world_diff: &WorldDiff) {
println!();
Expand All @@ -265,6 +295,7 @@ fn inspect_world(world_diff: &WorldDiff) {

let mut namespaces_disp = vec![];
let mut contracts_disp = vec![];
let mut external_contracts_disp = vec![];
let mut models_disp = vec![];
let mut events_disp = vec![];

Expand All @@ -290,15 +321,21 @@ fn inspect_world(world_diff: &WorldDiff) {
}
}

for contract in world_diff.external_contracts.values() {
external_contracts_disp.push(external_contract_diff_display(contract));
}

namespaces_disp.sort_by_key(|m| m.name.to_string());
contracts_disp.sort_by_key(|m| m.tag.to_string());
models_disp.sort_by_key(|m| m.tag.to_string());
events_disp.sort_by_key(|m| m.tag.to_string());
external_contracts_disp.sort_by_key(|c| format!("{}-{}", c.contract_name, c.instance_name));

print_table(&namespaces_disp, Some(Color::FG_BRIGHT_BLACK), None);
print_table(&contracts_disp, Some(Color::FG_BRIGHT_BLACK), None);
print_table(&models_disp, Some(Color::FG_BRIGHT_BLACK), None);
print_table(&events_disp, Some(Color::FG_BRIGHT_BLACK), None);
print_table(&external_contracts_disp, Some(Color::FG_BRIGHT_BLACK), None);
}

/// Displays the resource diff with the address and class hash.
Expand Down Expand Up @@ -428,6 +465,24 @@ fn resource_diff_display(world_diff: &WorldDiff, resource: &ResourceDiff) -> Res
}
}

/// Displays the external contract diff.
fn external_contract_diff_display(contract: &ExternalContractDiff) -> ExternalContractInspect {
let contract_data = contract.contract_data();

ExternalContractInspect {
contract_name: contract_data.contract_name,
instance_name: contract_data.instance_name,
address: contract_data.address.to_fixed_hex_string(),
class_hash: contract_data.class_hash.to_fixed_hex_string(),
status: match contract {
ExternalContractDiff::Created(_) => ResourceStatus::Created,
ExternalContractDiff::Synced(_) => ResourceStatus::Synced,
},
salt: contract_data.salt.to_fixed_hex_string(),
constructor_calldata: contract_data.constructor_data,
}
}

/// Prints a table.
fn print_table<T>(data: T, color: Option<Color>, title: Option<&str>)
where
Expand All @@ -452,12 +507,16 @@ where
println!("{table}\n");
}

/// Pretty prints a section header
fn print_section_header(str: &str) {
println!("\n{}", str.blue());
}

/// Pretty prints a TOML string.
fn pretty_print_toml(str: &str) {
for line in str.lines() {
if line.starts_with("[") {
// Print section headers.
println!("\n{}", line.blue());
print_section_header(line);
} else if line.contains('=') {
// Print key-value pairs with keys in green and values.
let parts: Vec<&str> = line.splitn(2, '=').collect();
Expand Down
Loading

0 comments on commit f0c1e0b

Please sign in to comment.