Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cast): add --from-rlp & --to-rlp #1465

Merged
merged 12 commits into from
Jun 7, 2022
45 changes: 44 additions & 1 deletion cast/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
//! Cast
//!
//! TODO
use crate::rlp_converter::Item;
use chrono::NaiveDateTime;
use ethers_core::{
abi::{
token::{LenientTokenizer, Tokenizer},
Abi, AbiParser, Token,
},
types::{Chain, *},
utils::{self, get_contract_address, keccak256, parse_units},
utils::{self, get_contract_address, keccak256, parse_units, rlp},
};
use ethers_etherscan::Client;
use ethers_providers::{Middleware, PendingTransaction};
Expand All @@ -17,11 +18,13 @@ pub use foundry_evm::*;
use foundry_utils::encode_args;
use print_utils::{get_pretty_block_attr, get_pretty_tx_attr, get_pretty_tx_receipt_attr, UIfmt};
use rustc_hex::{FromHexIter, ToHex};
use serde_json::Value;
use std::{path::PathBuf, str::FromStr};
pub use tx::TxBuilder;
use tx::{TxBuilderOutput, TxBuilderPeekOutput};

mod print_utils;
mod rlp_converter;
mod tx;

// TODO: CastContract with common contract initializers? Same for CastProviders?
Expand Down Expand Up @@ -1081,6 +1084,46 @@ impl SimpleCast {
}?)
}

/// Encodes hex data or list of hex data to hexadecimal rlp
///
/// ```
/// use cast::SimpleCast as Cast;
///
/// fn main() -> eyre::Result<()> {
/// assert_eq!(Cast::to_rlp("[]").unwrap(),"0xc0".to_string());
/// assert_eq!(Cast::to_rlp("0x22").unwrap(),"0x22".to_string());
/// assert_eq!(Cast::to_rlp("[\"0x61\"]",).unwrap(), "0xc161".to_string());
/// assert_eq!(Cast::to_rlp( "[\"0xf1\",\"f2\"]").unwrap(), "0xc481f181f2".to_string());
/// Ok(())
/// }
/// ```
pub fn to_rlp(value: &str) -> Result<String> {
let val = serde_json::from_str(value).unwrap_or(Value::String(value.parse()?));
let item = Item::value_to_item(&val)?;
Ok(format!("0x{}", hex::encode(rlp::encode(&item))))
}

/// Decodes rlp encoded list with hex data
///
/// ```
/// use cast::SimpleCast as Cast;
///
/// fn main() -> eyre::Result<()> {
/// assert_eq!(Cast::from_rlp("0xc0".to_string()).unwrap(), "[]");
/// assert_eq!(Cast::from_rlp("0x0f".to_string()).unwrap(), "\"0x0f\"");
/// assert_eq!(Cast::from_rlp("0x33".to_string()).unwrap(), "\"0x33\"");
/// assert_eq!(Cast::from_rlp("0xc161".to_string()).unwrap(), "[\"0x61\"]");
/// assert_eq!(Cast::from_rlp("0xc26162".to_string()).unwrap(), "[\"0x61\",\"0x62\"]");
/// Ok(())
/// }
/// ```
pub fn from_rlp(value: String) -> Result<String> {
let striped_value = strip_0x(&value);
let bytes = hex::decode(striped_value).expect("Could not decode hex");
let item = rlp::decode::<Item>(&bytes).expect("Could not decode rlp");
Ok(format!("{}", item))
}

/// Converts an Ethereum address to its checksum format
/// according to [EIP-55](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md)
///
Expand Down
176 changes: 176 additions & 0 deletions cast/src/rlp_converter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
use ethers_core::utils::rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream};
use serde_json::Value;
use std::fmt::{Debug, Display, Formatter};

/// Arbitrarly nested data
/// Item::Array(vec![]); is equivalent to []
/// Item::Array(vec![Item::Data(vec![])]); is equivalent to [""] or [null]
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Item {
Data(Vec<u8>),
Array(Vec<Item>),
}

impl Encodable for Item {
fn rlp_append(&self, s: &mut RlpStream) {
match self {
Item::Array(arr) => {
s.begin_unbounded_list();
for item in arr {
s.append(item);
}
s.finalize_unbounded_list();
}
Item::Data(data) => {
s.append(data);
}
}
}
}

impl Decodable for Item {
fn decode(rlp: &Rlp) -> std::result::Result<Self, DecoderError> {
if rlp.is_data() {
return Ok(Item::Data(Vec::from(rlp.data()?)))
}
Ok(Item::Array(rlp.as_list()?))
}
}

impl Item {
pub(crate) fn value_to_item(value: &Value) -> eyre::Result<Item> {
return match value {
Value::Null => Ok(Item::Data(vec![])),
Value::Bool(_) => {
eyre::bail!("RLP input should not contain booleans")
}
// If a value is passed without quotes we cast it to string
Value::Number(n) => Ok(Item::value_to_item(&Value::String(n.to_string()))?),
Value::String(s) => {
let hex_string = s.strip_prefix("0x").unwrap_or(s);
Ok(Item::Data(hex::decode(hex_string).expect("Could not decode hex")))
}
Value::Array(values) => values.iter().map(Item::value_to_item).collect(),
Value::Object(_) => {
eyre::bail!("RLP input can not contain objects")
}
}
}
}

impl FromIterator<Item> for Item {
fn from_iter<T: IntoIterator<Item = Item>>(iter: T) -> Self {
Item::Array(iter.into_iter().collect())
}
}

// Display as hex values
impl Display for Item {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Item::Data(dat) => {
write!(f, "\"0x{}\"", hex::encode(dat))?;
}
Item::Array(arr) => {
write!(f, "[")?;
for item in arr {
if arr.last() == Some(item) {
write!(f, "{item}")?;
} else {
write!(f, "{item},")?;
}
}
write!(f, "]")?;
}
};
Ok(())
}
}

#[macro_use]
#[cfg(test)]
mod test {
use crate::rlp_converter::Item;
use ethers_core::utils::{rlp, rlp::DecoderError};
use serde_json::Result as JsonResult;

// https://en.wikipedia.org/wiki/Set-theoretic_definition_of_natural_numbers
fn array_von_neuman() -> Item {
Item::Array(vec![
Item::Array(vec![]),
Item::Array(vec![Item::Array(vec![])]),
Item::Array(vec![Item::Array(vec![]), Item::Array(vec![Item::Array(vec![])])]),
])
}

#[test]
fn encode_decode_test() -> Result<(), DecoderError> {
let parameters = vec![
(1, b"\xc0".to_vec(), Item::Array(vec![])),
(2, b"\xc1\x80".to_vec(), Item::Array(vec![Item::Data(vec![])])),
(3, b"\xc4\x83dog".to_vec(), Item::Array(vec![Item::Data(vec![0x64, 0x6f, 0x67])])),
(
4,
b"\xc5\xc4\x83dog".to_vec(),
Item::Array(vec![Item::Array(vec![Item::Data(vec![0x64, 0x6f, 0x67])])]),
),
(
5,
b"\xc8\x83dog\x83cat".to_vec(),
Item::Array(vec![
Item::Data(vec![0x64, 0x6f, 0x67]),
Item::Data(vec![0x63, 0x61, 0x74]),
]),
),
(6, b"\xc7\xc0\xc1\xc0\xc3\xc0\xc1\xc0".to_vec(), array_von_neuman()),
(
7,
b"\xcd\x83\x6c\x6f\x6c\xc3\xc2\xc1\xc0\xc4\x83\x6f\x6c\x6f".to_vec(),
Item::Array(vec![
Item::Data(vec![b'\x6c', b'\x6f', b'\x6c']),
Item::Array(vec![Item::Array(vec![Item::Array(vec![Item::Array(vec![])])])]),
Item::Array(vec![Item::Data(vec![b'\x6f', b'\x6c', b'\x6f'])]),
]),
),
];
for params in parameters {
let encoded = rlp::encode::<Item>(&params.2);
assert_eq!(rlp::decode::<Item>(&encoded)?, params.2);
let decoded = rlp::decode::<Item>(&params.1);
assert_eq!(rlp::encode::<Item>(&decoded?), params.1);
println!("case {} validated", params.0)
}

Ok(())
}

#[test]
fn deserialize_from_str_test_hex() -> JsonResult<()> {
let parameters = vec![
(1, "[\"\"]", Item::Array(vec![Item::Data(vec![])])),
(2, "[\"0x646f67\"]", Item::Array(vec![Item::Data(vec![0x64, 0x6f, 0x67])])),
(
3,
"[[\"646f67\"]]",
Item::Array(vec![Item::Array(vec![Item::Data(vec![0x64, 0x6f, 0x67])])]),
),
(
4,
"[\"646f67\",\"0x636174\"]",
Item::Array(vec![
Item::Data(vec![0x64, 0x6f, 0x67]),
Item::Data(vec![0x63, 0x61, 0x74]),
]),
),
(6, "[[],[[]],[[],[[]]]]", array_von_neuman()),
];
for params in parameters {
let val = serde_json::from_str(params.1)?;
let item = Item::value_to_item(&val).unwrap();
assert_eq!(item, params.2);
println!("case {} validated", params.0);
}

Ok(())
}
}
8 changes: 8 additions & 0 deletions cli/src/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,14 @@ async fn main() -> eyre::Result<()> {
)?
);
}
Subcommands::ToRlp { value } => {
let val = unwrap_or_stdin(value)?;
println!("{}", SimpleCast::to_rlp(&val)?);
}
Subcommands::FromRlp { value } => {
let val = unwrap_or_stdin(value)?;
println!("{}", SimpleCast::from_rlp(val)?);
}
Subcommands::AccessList { eth, address, sig, args, block, to_json } => {
let config = Config::from(&eth);
let provider = Provider::try_from(
Expand Down
6 changes: 6 additions & 0 deletions cli/src/opts/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,12 @@ Examples:
#[clap(value_name = "UNIT")]
unit: Option<String>,
},
#[clap(name = "--to-rlp")]
#[clap(about = "RLP encodes hex data, or an array of hex data")]
ToRlp { value: Option<String> },
#[clap(name = "--from-rlp")]
#[clap(about = "Decodes RLP encoded data. Input must be hexadecimal.")]
FromRlp { value: Option<String> },
#[clap(name = "access-list")]
#[clap(visible_aliases = &["ac", "acl"])]
#[clap(about = "Create an access list for a transaction.")]
Expand Down
12 changes: 12 additions & 0 deletions cli/tests/it/cast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,15 @@ casttest!(upload_signatures, |_: TestProject, mut cmd: TestCommand| {
assert!(output.contains("Function decimals(): 0x313ce567"), "{}", output);
assert!(output.contains("Function allowance(address,address): 0xdd62ed3e"), "{}", output);
});

// tests that the `cast to-rlp` and `cast from-rlp` commands work correctly
casttest!(cast_rlp, |_: TestProject, mut cmd: TestCommand| {
cmd.args(["--to-rlp", "[\"0xaa\", [[\"bb\"]], \"0xcc\"]"]);
let out = cmd.stdout_lossy();
assert!(out.contains("0xc881aac3c281bb81cc"), "{}", out);

cmd.cast_fuse();
cmd.args(["--from-rlp", "0xcbc58455556666c0c0c2c1c0"]);
let out = cmd.stdout_lossy();
assert!(out.contains("[[\"0x55556666\"],[],[],[[[]]]]"), "{}", out);
});