Skip to content

Commit

Permalink
feat: cainome cli update and new config parsing (#22)
Browse files Browse the repository at this point in the history
* fix: ensure correct contract names and sierra parsing

* feat: add parser config file for type aliases and sierra extension

* feat: add contract aliases functionality
  • Loading branch information
glihm authored Apr 24, 2024
1 parent 07ba9c9 commit 075ef03
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 44 deletions.
5 changes: 5 additions & 0 deletions parser_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"sierra_extension": ".contract_class.json",
"contract_aliases": {},
"type_aliases": {}
}
5 changes: 5 additions & 0 deletions src/bin/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ pub struct CainomeArgs {
)]
pub artifacts_path: Option<Utf8PathBuf>,

#[arg(long)]
#[arg(value_name = "PATH")]
#[arg(help = "Path of a JSON file defining Cainome parsing configuration.")]
pub parser_config: Option<Utf8PathBuf>,

#[arg(long)]
#[arg(value_name = "ADDRESS")]
#[arg(conflicts_with = "artifacts_path")]
Expand Down
71 changes: 48 additions & 23 deletions src/bin/cli/contract/mod.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
use cainome_parser::{AbiParser, TokenizedAbi};
use camino::Utf8PathBuf;
use convert_case::{Case, Casing};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use url::Url;

// use starknet::core::types::contract::*;

use starknet::{
core::types::{BlockId, BlockTag, ContractClass, FieldElement},
providers::{jsonrpc::HttpTransport, AnyProvider, JsonRpcClient, Provider},
};

use crate::error::{CainomeCliResult, Error};

const SIERRA_EXT: &str = ".contract_class.json";

#[derive(Debug)]
pub enum ContractOrigin {
/// Contract's ABI was loaded from a local Sierra class file
Expand All @@ -35,10 +31,41 @@ pub struct ContractData {
pub tokens: TokenizedAbi,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContractParserConfig {
/// The file extension that should be considered as a Sierra file.
pub sierra_extension: String,
/// The type aliases to be provided to the Cainome parser.
pub type_aliases: HashMap<String, String>,
/// The contract aliases to be provided to the Cainome parser.
pub contract_aliases: HashMap<String, String>,
}

impl ContractParserConfig {
pub fn from_json(path: &Utf8PathBuf) -> CainomeCliResult<Self> {
Ok(serde_json::from_reader(std::io::BufReader::new(
std::fs::File::open(path)?,
))?)
}
}

impl Default for ContractParserConfig {
fn default() -> Self {
Self {
sierra_extension: ".contract_class.json".to_string(),
type_aliases: HashMap::default(),
contract_aliases: HashMap::default(),
}
}
}

pub struct ContractParser {}

impl ContractParser {
pub fn from_artifacts_path(path: Utf8PathBuf) -> CainomeCliResult<Vec<ContractData>> {
pub fn from_artifacts_path(
path: Utf8PathBuf,
config: &ContractParserConfig,
) -> CainomeCliResult<Vec<ContractData>> {
let mut contracts = vec![];

for entry in fs::read_dir(path)? {
Expand All @@ -47,23 +74,25 @@ impl ContractParser {

if path.is_file() {
if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
if !file_name.ends_with(SIERRA_EXT) {
if !file_name.ends_with(&config.sierra_extension) {
continue;
}

let file_content = fs::read_to_string(&path)?;

// TODO: check how the aliases can be passed to the CLI....!
// It's a simple HashMap<String, String>, flat file with two columns
// may be ok?
let aliases = HashMap::new();

match AbiParser::tokens_from_abi_string(&file_content, &aliases) {
match AbiParser::tokens_from_abi_string(&file_content, &config.type_aliases) {
Ok(tokens) => {
let contract_name = file_name
.trim_end_matches(SIERRA_EXT)
.from_case(Case::Snake)
.to_case(Case::Pascal);
let contract_name = {
let n = file_name.trim_end_matches(&config.sierra_extension);
if let Some(alias) = config.contract_aliases.get(n) {
tracing::trace!(
"Aliasing {file_name} contract name with {alias}"
);
alias
} else {
n
}
};

tracing::trace!(
"Adding {contract_name} ({file_name}) to the list of contracts"
Expand All @@ -89,21 +118,17 @@ impl ContractParser {
name: &str,
address: FieldElement,
rpc_url: Url,
type_aliases: &HashMap<String, String>,
) -> CainomeCliResult<ContractData> {
let provider = AnyProvider::JsonRpcHttp(JsonRpcClient::new(HttpTransport::new(rpc_url)));

let class = provider
.get_class_at(BlockId::Tag(BlockTag::Latest), address)
.await?;

// TODO: check how the aliases can be passed to the CLI....!
// It's a simple HashMap<String, String>, flat file with two columns
// may be ok?
let aliases = HashMap::new();

match class {
ContractClass::Sierra(sierra) => {
match AbiParser::tokens_from_abi_string(&sierra.abi, &aliases) {
match AbiParser::tokens_from_abi_string(&sierra.abi, type_aliases) {
Ok(tokens) => Ok(ContractData {
name: name.to_string(),
origin: ContractOrigin::FetchedFromChain(address),
Expand Down
56 changes: 38 additions & 18 deletions src/bin/cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,45 +7,65 @@ mod error;
mod plugins;

use args::CainomeArgs;
use contract::ContractParser;
use contract::{ContractParser, ContractParserConfig};
use error::{CainomeCliResult, Error};
use plugins::{PluginInput, PluginManager};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
async fn main() -> CainomeCliResult<()> {
init_logging()?;

let config = CainomeArgs::parse();
tracing::trace!("config: {:?}", config);

let contracts = if let Some(path) = config.artifacts_path {
ContractParser::from_artifacts_path(path)?
} else if let (Some(name), Some(address), Some(url)) = (
config.contract_name,
config.contract_address,
config.rpc_url,
) {
vec![ContractParser::from_chain(&name, address, url).await?]
let args = CainomeArgs::parse();
tracing::trace!("args: {:?}", args);

let parser_config = if let Some(path) = args.parser_config {
ContractParserConfig::from_json(&path)?
} else {
ContractParserConfig::default()
};

let contracts = if let Some(path) = args.artifacts_path {
let ret = ContractParser::from_artifacts_path(path.clone(), &parser_config)?;

if ret.is_empty() {
tracing::error!(
"No contract found with extension '{}' into '{}' directory",
parser_config.sierra_extension,
path
);

return Err(Error::Other("Invalid arguments".to_string()));
}

ret
} else if let (Some(name), Some(address), Some(url)) =
(args.contract_name, args.contract_address, args.rpc_url)
{
vec![ContractParser::from_chain(&name, address, url, &parser_config.type_aliases).await?]
} else {
panic!("Invalid arguments: no contracts to be parsed");
};

let pm = PluginManager::from(config.plugins);
let pm = PluginManager::from(args.plugins);

pm.generate(PluginInput {
output_dir: config.output_dir,
output_dir: args.output_dir,
contracts,
})
.await?;

Ok(())
}

pub fn init_logging() -> Result<(), Box<dyn std::error::Error>> {
pub fn init_logging() -> CainomeCliResult<()> {
const DEFAULT_LOG_FILTER: &str = "info,cainome=trace";

let builder = fmt::Subscriber::builder().with_env_filter(
EnvFilter::try_from_default_env().or(EnvFilter::try_new(DEFAULT_LOG_FILTER))?,
EnvFilter::try_from_default_env()
.or(EnvFilter::try_new(DEFAULT_LOG_FILTER))
.map_err(|e| Error::Other(format!("Tracing error: {:?}", e)))?,
);

Ok(tracing::subscriber::set_global_default(builder.finish())?)
tracing::subscriber::set_global_default(builder.finish())
.map_err(|e| Error::Other(format!("Tracing error: {:?}", e)))
}
22 changes: 19 additions & 3 deletions src/bin/cli/plugins/builtins/rust.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use async_trait::async_trait;
use cainome_rs::{self};
use convert_case::{Case, Casing};

use crate::error::CainomeCliResult;
use crate::plugins::builtins::BuiltinPlugin;
Expand All @@ -16,11 +17,26 @@ impl RustPlugin {
#[async_trait]
impl BuiltinPlugin for RustPlugin {
async fn generate_code(&self, input: &PluginInput) -> CainomeCliResult<()> {
tracing::trace!("Rust plugin requested:\n{:?}\n", input);
tracing::trace!("Rust plugin requested");

for contract in &input.contracts {
let expanded = cainome_rs::abi_to_tokenstream(&contract.name, &contract.tokens);
let filename = format!("{}.rs", contract.name);
// The contract name contains the fully qualified path of the cairo module.
// For now, let's only take the latest part of this path.
// TODO: if a project has several contracts with the same name under different
// namespaces, we should provide a solution to solve those conflicts.
let contract_name = contract
.name
.split("::")
.last()
.unwrap_or(&contract.name)
.from_case(Case::Snake)
.to_case(Case::Pascal);

let expanded = cainome_rs::abi_to_tokenstream(&contract_name, &contract.tokens);
let filename = format!(
"{}.rs",
contract_name.from_case(Case::Pascal).to_case(Case::Snake)
);

let mut out_path = input.output_dir.clone();
out_path.push(filename);
Expand Down

0 comments on commit 075ef03

Please sign in to comment.