Skip to content

Commit

Permalink
feat: parse fetched classes into original artifacts
Browse files Browse the repository at this point in the history
Adds a new `--parse` flag to the `class-at` and `class-by-hash`
commands. When used, Starkli attempts to parse the raw JSON-RPC class
response back into the original contract artifact format (same as the
output from a compiler).

With this flag, users can retrieve the full original contract artifact
solely from a contract address or class hash. This is helpful when
trying to re-declare a certain class into another network.
  • Loading branch information
xJonathanLEI committed Dec 14, 2023
1 parent e35c105 commit 08b9571
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 18 deletions.
5 changes: 3 additions & 2 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ clap_complete = "4.3.1"
colored = "2.0.0"
colored_json = "3.2.0"
env_logger = "0.10.0"
flate2 = "1.0.28"
hex = "0.4.3"
hex-literal = "0.4.1"
log = "0.4.19"
Expand Down
32 changes: 25 additions & 7 deletions src/subcommands/class_at.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
use anyhow::Result;
use clap::Parser;
use colored_json::{ColorMode, Output};
use starknet::{
core::types::{BlockId, BlockTag, FieldElement},
core::types::{BlockId, BlockTag, ContractClass, FieldElement},
providers::Provider,
};

use crate::{verbosity::VerbosityArgs, ProviderArgs};
use crate::{
utils::{parse_compressed_legacy_class, parse_flattened_sierra_class, print_colored_json},
verbosity::VerbosityArgs,
ProviderArgs,
};

#[derive(Debug, Parser)]
pub struct ClassAt {
#[clap(flatten)]
provider: ProviderArgs,
#[clap(
long,
help = "Attempt to recover a flattened Sierra class or a compressed legacy class"
)]
parse: bool,
#[clap(help = "Contract address")]
address: String,
#[clap(flatten)]
Expand All @@ -30,10 +38,20 @@ impl ClassAt {
.get_class_at(BlockId::Tag(BlockTag::Pending), address)
.await?;

let class_json = serde_json::to_value(class)?;
let class_json =
colored_json::to_colored_json(&class_json, ColorMode::Auto(Output::StdOut))?;
println!("{class_json}");
if self.parse {
match class {
ContractClass::Sierra(class) => {
let class = parse_flattened_sierra_class(class)?;
print_colored_json(&class)?;
}
ContractClass::Legacy(class) => {
let class = parse_compressed_legacy_class(class)?;
print_colored_json(&class)?;
}
}
} else {
print_colored_json(&serde_json::to_value(class)?)?;
}

Ok(())
}
Expand Down
32 changes: 25 additions & 7 deletions src/subcommands/class_by_hash.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
use anyhow::Result;
use clap::Parser;
use colored_json::{ColorMode, Output};
use starknet::{
core::types::{BlockId, BlockTag, FieldElement},
core::types::{BlockId, BlockTag, ContractClass, FieldElement},
providers::Provider,
};

use crate::{verbosity::VerbosityArgs, ProviderArgs};
use crate::{
utils::{parse_compressed_legacy_class, parse_flattened_sierra_class, print_colored_json},
verbosity::VerbosityArgs,
ProviderArgs,
};

#[derive(Debug, Parser)]
pub struct ClassByHash {
#[clap(flatten)]
provider: ProviderArgs,
#[clap(
long,
help = "Attempt to recover a flattened Sierra class or a compressed legacy class"
)]
parse: bool,
#[clap(help = "Class hash")]
hash: String,
#[clap(flatten)]
Expand All @@ -30,10 +38,20 @@ impl ClassByHash {
.get_class(BlockId::Tag(BlockTag::Pending), class_hash)
.await?;

let class_json = serde_json::to_value(class)?;
let class_json =
colored_json::to_colored_json(&class_json, ColorMode::Auto(Output::StdOut))?;
println!("{class_json}");
if self.parse {
match class {
ContractClass::Sierra(class) => {
let class = parse_flattened_sierra_class(class)?;
print_colored_json(&class)?;
}
ContractClass::Legacy(class) => {
let class = parse_compressed_legacy_class(class)?;
print_colored_json(&class)?;
}
}
} else {
print_colored_json(&serde_json::to_value(class)?)?;
}

Ok(())
}
Expand Down
133 changes: 131 additions & 2 deletions src/utils.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
use std::time::Duration;
use std::{io::Read, time::Duration};

use anyhow::Result;
use bigdecimal::{BigDecimal, Zero};
use colored::Colorize;
use colored_json::{ColorMode, ColoredFormatter, Output};
use flate2::read::GzDecoder;
use num_integer::Integer;
use regex::Regex;
use serde::Serialize;
use serde_json::ser::PrettyFormatter;
use starknet::{
core::types::{BlockId, BlockTag, ExecutionResult, FieldElement, StarknetError},
core::types::{
contract::{
legacy::{
LegacyContractClass, LegacyEntrypointOffset, LegacyProgram, RawLegacyEntryPoint,
RawLegacyEntryPoints,
},
AbiEntry, SierraClass, SierraClassDebugInfo,
},
BlockId, BlockTag, CompressedLegacyContractClass, ExecutionResult, FieldElement,
FlattenedSierraClass, LegacyContractEntryPoint, StarknetError,
},
providers::{MaybeUnknownErrorCode, Provider, ProviderError, StarknetErrorWithMessage},
};

Expand Down Expand Up @@ -110,3 +124,118 @@ where

Ok(FieldElement::from_byte_slice_be(&biguint.to_bytes_be())?)
}

/// Prints colored JSON for any serializable value. This is better then directly calling
/// `colored_json::to_colored_json` as that method only takes `serde_json::Value`. Unfortunately,
/// converting certain values to `serde_json::Value` would result in data loss.
pub fn print_colored_json<T>(value: &T) -> Result<()>
where
T: Serialize,
{
let mut writer = Vec::with_capacity(128);

if ColorMode::Auto(Output::StdOut).use_color() {
let formatter = ColoredFormatter::new(PrettyFormatter::new());
let mut serializer = serde_json::Serializer::with_formatter(&mut writer, formatter);
value.serialize(&mut serializer)?;
} else {
let formatter = PrettyFormatter::new();
let mut serializer = serde_json::Serializer::with_formatter(&mut writer, formatter);
value.serialize(&mut serializer)?;
}

let json = unsafe {
// `serde_json` and `colored_json` do not emit invalid UTF-8.
String::from_utf8_unchecked(writer)
};

println!("{}", json);

Ok(())
}

/// Attempts to recover a flattened Sierra class by parsing its ABI string. This works only if the
/// declared ABI string is a valid JSON representation of Seirra ABI.
pub fn parse_flattened_sierra_class(class: FlattenedSierraClass) -> Result<SierraClass> {
Ok(SierraClass {
sierra_program: class.sierra_program,
sierra_program_debug_info: SierraClassDebugInfo {
type_names: vec![],
libfunc_names: vec![],
user_func_names: vec![],
},
contract_class_version: class.contract_class_version,
entry_points_by_type: class.entry_points_by_type,
abi: serde_json::from_str::<Vec<AbiEntry>>(&class.abi)?,
})
}

/// Attempts to recover a compressed legacy program.
pub fn parse_compressed_legacy_class(
class: CompressedLegacyContractClass,
) -> Result<LegacyContractClass> {
let mut gzip_decoder = GzDecoder::new(class.program.as_slice());
let mut program_json = String::new();
gzip_decoder.read_to_string(&mut program_json)?;

let program = serde_json::from_str::<LegacyProgram>(&program_json)?;

let is_pre_0_11_0 = match &program.compiler_version {
Some(compiler_version) => {
let minor_version = compiler_version
.split('.')
.nth(1)
.ok_or_else(|| anyhow::anyhow!("unexpected legacy compiler version string"))?;

let minor_version: u8 = minor_version.parse()?;
minor_version < 11
}
None => true,
};

let abi = match class.abi {
Some(abi) => abi.into_iter().map(|item| item.into()).collect(),
None => vec![],
};

Ok(LegacyContractClass {
abi,
entry_points_by_type: RawLegacyEntryPoints {
constructor: class
.entry_points_by_type
.constructor
.into_iter()
.map(|item| parse_legacy_entrypoint(&item, is_pre_0_11_0))
.collect(),
external: class
.entry_points_by_type
.external
.into_iter()
.map(|item| parse_legacy_entrypoint(&item, is_pre_0_11_0))
.collect(),
l1_handler: class
.entry_points_by_type
.l1_handler
.into_iter()
.map(|item| parse_legacy_entrypoint(&item, is_pre_0_11_0))
.collect(),
},
program,
})
}

fn parse_legacy_entrypoint(
entrypoint: &LegacyContractEntryPoint,
pre_0_11_0: bool,
) -> RawLegacyEntryPoint {
RawLegacyEntryPoint {
// This doesn't really matter as it doesn't affect class hashes. We simply try to guess as
// close as possible.
offset: if pre_0_11_0 {
LegacyEntrypointOffset::U64AsHex(entrypoint.offset)
} else {
LegacyEntrypointOffset::U64AsInt(entrypoint.offset)
},
selector: entrypoint.selector,
}
}

0 comments on commit 08b9571

Please sign in to comment.