diff --git a/Cargo.lock b/Cargo.lock index e02023ea3132..bdc03f3a720e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,6 +327,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00" +dependencies = [ + "num", + "time 0.1.43", +] + [[package]] name = "chrono" version = "0.4.19" @@ -1441,7 +1451,7 @@ checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if", "js-sys", - "time", + "time 0.2.27", "wasm-bindgen", "web-sys", ] @@ -1601,6 +1611,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" +dependencies = [ + "num-integer", + "num-iter", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -1611,6 +1632,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2021c8337a54d21aca0d59a92577a029af9431cb59b909b03252b9c164fad59" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.14" @@ -2339,6 +2371,7 @@ dependencies = [ name = "seth" version = "0.1.0" dependencies = [ + "chrono 0.2.25", "dapp-utils", "ethers-core", "ethers-providers", @@ -2672,6 +2705,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "time" version = "0.2.27" @@ -2885,7 +2928,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62af966210b88ad5776ee3ba12d5f35b8d6a2b2a12168f3080cf02b814d7376b" dependencies = [ "ansi_term 0.12.1", - "chrono", + "chrono 0.4.19", "lazy_static", "matchers", "regex", diff --git a/dapptools/src/seth.rs b/dapptools/src/seth.rs index ae31f24bc7c1..b9fb465cfb79 100644 --- a/dapptools/src/seth.rs +++ b/dapptools/src/seth.rs @@ -1,14 +1,14 @@ mod seth_opts; use seth_opts::{Opts, Subcommands}; -use seth::{Seth, SimpleSeth}; - use ethers::{ + core::types::{BlockId, BlockNumber::Latest}, middleware::SignerMiddleware, providers::{Middleware, Provider}, signers::Signer, types::NameOrAddress, }; +use seth::{Seth, SimpleSeth}; use std::{convert::TryFrom, str::FromStr}; use structopt::StructOpt; @@ -25,9 +25,33 @@ async fn main() -> eyre::Result<()> { Subcommands::ToCheckSumAddress { address } => { println!("{}", SimpleSeth::checksum_address(&address)?); } + Subcommands::ToAscii { hexdata } => { + println!("{}", SimpleSeth::to_ascii(&hexdata)?); + } Subcommands::ToBytes32 { bytes } => { println!("{}", SimpleSeth::bytes32(&bytes)?); } + Subcommands::ToDec { hexvalue } => { + println!("{}", SimpleSeth::to_dec(&hexvalue)?); + } + Subcommands::ToFix { decimals, value } => { + println!( + "{}", + SimpleSeth::to_fix(unwrap_or_stdin(decimals)?, unwrap_or_stdin(value)?)? + ); + } + Subcommands::ToUint256 { value } => { + println!("{}", SimpleSeth::to_uint256(value)?); + } + Subcommands::ToWei { value, unit } => { + println!( + "{}", + SimpleSeth::to_wei( + unwrap_or_stdin(value)?, + unit.unwrap_or_else(|| String::from("wei")) + )? + ); + } Subcommands::Block { rpc_url, block, @@ -43,6 +67,10 @@ async fn main() -> eyre::Result<()> { .await? ); } + Subcommands::BlockNumber { rpc_url } => { + let provider = Provider::try_from(rpc_url)?; + println!("{}", Seth::new(provider).block_number().await?); + } Subcommands::Call { rpc_url, address, @@ -52,6 +80,17 @@ async fn main() -> eyre::Result<()> { let provider = Provider::try_from(rpc_url)?; println!("{}", Seth::new(provider).call(address, &sig, args).await?); } + Subcommands::Chain { rpc_url } => { + let provider = Provider::try_from(rpc_url)?; + println!("{}", Seth::new(provider).chain().await?); + } + Subcommands::ChainId { rpc_url } => { + let provider = Provider::try_from(rpc_url)?; + println!("{}", Seth::new(provider).chain_id().await?); + } + Subcommands::Namehash { name } => { + println!("{}", SimpleSeth::namehash(&name)?); + } Subcommands::SendTx { eth, to, sig, args } => { let provider = Provider::try_from(eth.rpc_url.as_str())?; if let Some(signer) = eth.signer()? { @@ -63,6 +102,15 @@ async fn main() -> eyre::Result<()> { seth_send(provider, from, to, sig, args, eth.seth_async).await?; } } + Subcommands::Age { block, rpc_url } => { + let provider = Provider::try_from(rpc_url)?; + println!( + "{}", + Seth::new(provider) + .age(block.unwrap_or(BlockId::Number(Latest))) + .await? + ); + } Subcommands::Balance { block, who, @@ -71,6 +119,22 @@ async fn main() -> eyre::Result<()> { let provider = Provider::try_from(rpc_url)?; println!("{}", Seth::new(provider).balance(who, block).await?); } + Subcommands::BaseFee { block, rpc_url } => { + let provider = Provider::try_from(rpc_url)?; + println!( + "{}", + Seth::new(provider) + .base_fee(block.unwrap_or(BlockId::Number(Latest))) + .await? + ); + } + Subcommands::GasPrice { rpc_url } => { + let provider = Provider::try_from(rpc_url)?; + println!("{}", Seth::new(provider).gas_price().await?); + } + Subcommands::Keccak { data } => { + println!("{}", SimpleSeth::keccak(&data)?); + } Subcommands::ResolveName { who, rpc_url, diff --git a/dapptools/src/seth_opts.rs b/dapptools/src/seth_opts.rs index d5bc4d3b05d8..9a7b405bd800 100644 --- a/dapptools/src/seth_opts.rs +++ b/dapptools/src/seth_opts.rs @@ -21,9 +21,30 @@ pub enum Subcommands { #[structopt(name = "--to-checksum-address")] #[structopt(about = "convert an address to a checksummed format (EIP-55)")] ToCheckSumAddress { address: Address }, + #[structopt(name = "--to-ascii")] + #[structopt(about = "convert hex data to text data")] + ToAscii { hexdata: String }, #[structopt(name = "--to-bytes32")] #[structopt(about = "left-pads a hex bytes string to 32 bytes)")] ToBytes32 { bytes: String }, + #[structopt(name = "--to-dec")] + #[structopt(about = "convert hex value into decimal number")] + ToDec { hexvalue: String }, + #[structopt(name = "--to-fix")] + #[structopt(about = "convert integers into fixed point with specified decimals")] + ToFix { + decimals: Option, + value: Option, + }, + #[structopt(name = "--to-uint256")] + #[structopt(about = "convert a number into uint256 hex string with 0x prefix")] + ToUint256 { value: String }, + #[structopt(name = "--to-wei")] + #[structopt(about = "convert an ETH amount into wei")] + ToWei { + value: Option, + unit: Option, + }, #[structopt(name = "block")] #[structopt( about = "Prints information about . If is given, print only the value of that field" @@ -39,6 +60,12 @@ pub enum Subcommands { #[structopt(long, env = "ETH_RPC_URL")] rpc_url: String, }, + #[structopt(name = "block-number")] + #[structopt(about = "Prints latest block number")] + BlockNumber { + #[structopt(long, env = "ETH_RPC_URL")] + rpc_url: String, + }, #[structopt(name = "call")] #[structopt(about = "Perform a local call to without publishing a transaction.")] Call { @@ -49,6 +76,21 @@ pub enum Subcommands { #[structopt(long, env = "ETH_RPC_URL")] rpc_url: String, }, + #[structopt(name = "chain")] + #[structopt(about = "Prints symbolic name of current blockchain by checking genesis hash")] + Chain { + #[structopt(long, env = "ETH_RPC_URL")] + rpc_url: String, + }, + #[structopt(name = "chain-id")] + #[structopt(about = "returns ethereum chain id")] + ChainId { + #[structopt(long, env = "ETH_RPC_URL")] + rpc_url: String, + }, + #[structopt(name = "namehash")] + #[structopt(about = "returns ENS namehash of provided name")] + Namehash { name: String }, #[structopt(name = "send")] #[structopt(about = "Publish a transaction signed by to call with ")] SendTx { @@ -61,6 +103,14 @@ pub enum Subcommands { #[structopt(flatten)] eth: EthereumOpts, }, + #[structopt(name = "age")] + #[structopt(about = "Prints the timestamp of a block")] + Age { + #[structopt(global = true, help = "the block you want to query, can also be earliest/latest/pending", parse(try_from_str = parse_block_id))] + block: Option, + #[structopt(short, long, env = "ETH_RPC_URL")] + rpc_url: String, + }, #[structopt(name = "balance")] #[structopt(about = "Print the balance of in wei")] Balance { @@ -71,6 +121,23 @@ pub enum Subcommands { #[structopt(short, long, env = "ETH_RPC_URL")] rpc_url: String, }, + #[structopt(name = "basefee")] + #[structopt(about = "Print the basefee of a block")] + BaseFee { + #[structopt(global = true, help = "the block you want to query, can also be earliest/latest/pending", parse(try_from_str = parse_block_id))] + block: Option, + #[structopt(short, long, env = "ETH_RPC_URL")] + rpc_url: String, + }, + #[structopt(name = "gas-price")] + #[structopt(about = "Prints current gas price of target chain")] + GasPrice { + #[structopt(short, long, env = "ETH_RPC_URL")] + rpc_url: String, + }, + #[structopt(name = "keccak")] + #[structopt(about = "Keccak-256 hashes arbitrary data")] + Keccak { data: String }, #[structopt(name = "resolve-name")] #[structopt(about = "Returns the address the provided ENS name resolves to")] ResolveName { diff --git a/seth/Cargo.toml b/seth/Cargo.toml index 0deebe06b83b..cdbed789924c 100644 --- a/seth/Cargo.toml +++ b/seth/Cargo.toml @@ -13,3 +13,4 @@ ethers-providers = { git = "https://github.com/gakonst/ethers-rs", branch = "mas eyre = "0.6.5" rustc-hex = "2.1.0" serde_json = "1.0.67" +chrono = "0.2" diff --git a/seth/src/lib.rs b/seth/src/lib.rs index e8a10987dc4b..201061ed8735 100644 --- a/seth/src/lib.rs +++ b/seth/src/lib.rs @@ -1,10 +1,14 @@ //! Seth //! //! TODO -use ethers_core::{types::*, utils}; +use chrono::NaiveDateTime; +use ethers_core::{ + types::*, + utils::{self, keccak256}, +}; use ethers_providers::{Middleware, PendingTransaction}; use eyre::Result; -use rustc_hex::ToHex; +use rustc_hex::{FromHexIter, ToHex}; use std::str::FromStr; use dapp_utils::{encode_args, get_func, to_table}; @@ -195,6 +199,88 @@ where Ok(block) } + + async fn block_field_as_num>(&self, block: T, field: String) -> Result { + let block = block.into(); + let block_field = Seth::block( + self, + block, + false, + // Select only select field + Some(field), + false, + ) + .await?; + Ok(U256::from_str_radix(strip_0x(&block_field), 16) + .expect("Unable to convert hexadecimal to U256")) + } + + pub async fn base_fee>(&self, block: T) -> Result { + Ok(Seth::block_field_as_num(self, block, String::from("baseFeePerGas")).await?) + } + + pub async fn age>(&self, block: T) -> Result { + let timestamp_str = Seth::block_field_as_num(self, block, String::from("timestamp")) + .await? + .to_string(); + let datetime = NaiveDateTime::from_timestamp(timestamp_str.parse::().unwrap(), 0); + Ok(datetime.format("%a %b %e %H:%M:%S %Y").to_string()) + } + + pub async fn chain(&self) -> Result<&str> { + let genesis_hash = Seth::block( + self, + 0, + false, + // Select only block hash + Some(String::from("hash")), + false, + ) + .await?; + + Ok(match &genesis_hash[..] { + "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" => { + match &(Seth::block(self, 1920000, false, Some(String::from("hash")), false) + .await?)[..] + { + "0x94365e3a8c0b35089c1d1195081fe7489b528a84b22199c916180db8b28ade7f" => { + "etclive" + } + _ => "ethlive", + } + } + "0xa3c565fc15c7478862d50ccd6561e3c06b24cc509bf388941c25ea985ce32cb9" => "kovan", + "0x41941023680923e0fe4d74a34bdac8141f2540e3ae90623718e47d66d1ca4a2d" => "ropsten", + "0x39e1b9259598b65c8c71d1ea153de17e89222e64e8b271213dfb92c231f7fb88" => { + "optimism-mainnet" + } + "0x2510549c5c30f15472b55dbae139122e2e593f824217eefc7a53f78698ac5c1e" => { + "optimism-kovan" + } + "0x7ee576b35482195fc49205cec9af72ce14f003b9ae69f6ba0faef4514be8b442" => { + "arbitrum-mainnet" + } + "0x0cd786a2425d16f152c658316c423e6ce1181e15c3295826d7c9904cba9ce303" => "morden", + "0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177" => "rinkeby", + "0xbf7e331f7f7c1dd2e05159666b3bf8bc7a8a3a9eb1d518969eab529dd9b88c1a" => "goerli", + "0x14c2283285a88fe5fce9bf5c573ab03d6616695d717b12a127188bcacfc743c4" => "kotti", + "0x6d3c66c5357ec91d5c43af47e234a939b22557cbb552dc45bebbceeed90fbe34" => "bsctest", + "0x0d21840abff46b96c84b2ac9e10e4f5cdaeb5693cb665db62a2f3b02d2d57b5b" => "bsc", + _ => "unknown", + }) + } + + pub async fn chain_id(&self) -> Result { + Ok(self.provider.get_chainid().await?) + } + + pub async fn block_number(&self) -> Result { + Ok(self.provider.get_block_number().await?) + } + + pub async fn gas_price(&self) -> Result { + Ok(self.provider.get_gas_price().await?) + } } pub struct SimpleSeth; @@ -211,6 +297,73 @@ impl SimpleSeth { let s: String = s.as_bytes().to_hex(); format!("0x{}", s) } + + /// Converts hex data into text data + /// + /// ``` + /// use seth::SimpleSeth as Seth; + /// + /// fn main() -> eyre::Result<()> { + /// assert_eq!("Hello, World!", Seth::to_ascii("48656c6c6f2c20576f726c6421")?); + /// assert_eq!("TurboDappTools", Seth::to_ascii("0x547572626f44617070546f6f6c73")?); + /// + /// Ok(()) + /// } + /// ``` + pub fn to_ascii(hex: &str) -> Result { + let hex_trimmed = hex.trim_start_matches("0x"); + let iter = FromHexIter::new(hex_trimmed); + let mut ascii = String::new(); + for letter in iter.collect::>() { + ascii.push(letter.unwrap() as char); + } + Ok(ascii) + } + + /// Converts hex input to decimal + /// + /// ``` + /// use seth::SimpleSeth as Seth; + /// + /// fn main() -> eyre::Result<()> { + /// assert_eq!(424242, Seth::to_dec("0x67932")?); + /// assert_eq!(1234, Seth::to_dec("0x4d2")?); + /// + /// Ok(()) + /// } + pub fn to_dec(hex: &str) -> Result { + let hex_trimmed = hex.trim_start_matches("0x"); + Ok(u128::from_str_radix(hex_trimmed, 16)?) + } + + /// Converts integers with specified decimals into fixed point numbers + /// + /// ``` + /// use seth::SimpleSeth as Seth; + /// + /// fn main() -> eyre::Result<()> { + /// assert_eq!(Seth::to_fix(0, 10)?, "10."); + /// assert_eq!(Seth::to_fix(1, 10)?, "1.0"); + /// assert_eq!(Seth::to_fix(2, 10)?, "0.10"); + /// assert_eq!(Seth::to_fix(3, 10)?, "0.010"); + /// + /// Ok(()) + /// } + /// ``` + pub fn to_fix(decimals: u128, value: u128) -> Result { + let mut value: String = value.to_string(); + let decimals = decimals as usize; + + if decimals >= value.len() { + // {0}.{0 * (number_of_decimals - value.len())}{value} + Ok(format!("0.{:0>1$}", value, decimals)) + } else { + // Insert decimal at -idx (i.e 1 => decimal idx = -1) + value.insert(value.len() - decimals, '.'); + Ok(value) + } + } + /// Converts decimal input to hex /// /// ``` @@ -223,6 +376,51 @@ impl SimpleSeth { format!("{:#x}", u) } + /// Converts a number into uint256 hex string with 0x prefix + /// + /// ``` + /// use seth::SimpleSeth as Seth; + /// + /// fn main() -> eyre::Result<()> { + /// assert_eq!(Seth::to_uint256("100".to_string())?, "0x0000000000000000000000000000000000000000000000000000000000000064"); + /// assert_eq!(Seth::to_uint256("192038293923".to_string())?, "0x0000000000000000000000000000000000000000000000000000002cb65fd1a3"); + /// assert_eq!( + /// Seth::to_uint256("115792089237316195423570985008687907853269984665640564039457584007913129639935".to_string())?, + /// "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + /// ); + /// + /// Ok(()) + /// } + /// ``` + pub fn to_uint256(value: String) -> Result { + let num_u256 = U256::from_str_radix(&value, 10)?; + let num_hex = format!("{:x}", num_u256); + Ok(format!("0x{}{}", "0".repeat(64 - num_hex.len()), num_hex)) + } + + /// Converts an eth amount into wei + /// + /// ``` + /// use seth::SimpleSeth as Seth; + /// + /// fn main() -> eyre::Result<()> { + /// assert_eq!(Seth::to_wei(1, "".to_string())?, "1"); + /// assert_eq!(Seth::to_wei(100, "gwei".to_string())?, "100000000000"); + /// assert_eq!(Seth::to_wei(100, "eth".to_string())?, "100000000000000000000"); + /// assert_eq!(Seth::to_wei(1000, "ether".to_string())?, "1000000000000000000000"); + /// + /// Ok(()) + /// } + /// ``` + pub fn to_wei(value: u128, unit: String) -> Result { + let value = value.to_string(); + Ok(match &unit[..] { + "gwei" => format!("{:0<1$}", value, 9 + value.len()), + "eth" | "ether" => format!("{:0<1$}", value, 18 + value.len()), + _ => value, + }) + } + /// Converts an Ethereum address to its checksum format /// according to [EIP-55](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-55.md) /// @@ -269,6 +467,60 @@ impl SimpleSeth { // need to use the Debug implementation Ok(format!("{:?}", H256::from_str(&padded)?)) } + + /// Keccak-256 hashes arbitrary data + /// + /// ``` + /// use seth::SimpleSeth as Seth; + /// + /// fn main() -> eyre::Result<()> { + /// assert_eq!(Seth::keccak("foo")?, "0x41b1a0649752af1b28b3dc29a1556eee781e4a4c3a1f7f53f90fa834de098c4d"); + /// assert_eq!(Seth::keccak("123abc")?, "0xb1f1c74a1ba56f07a892ea1110a39349d40f66ca01d245e704621033cb7046a4"); + /// + /// Ok(()) + /// } + /// ``` + pub fn keccak(data: &str) -> Result { + let hash: String = keccak256(data.as_bytes()).to_hex(); + Ok(format!("0x{}", hash)) + } + + /// Converts ENS names to their namehash representation + /// [Namehash reference](https://docs.ens.domains/contract-api-reference/name-processing#hashing-names) + /// [namehash-rust reference](https://github.com/InstateDev/namehash-rust/blob/master/src/lib.rs) + /// + /// ``` + /// use seth::SimpleSeth as Seth; + /// + /// fn main() -> eyre::Result<()> { + /// assert_eq!(Seth::namehash("")?, "0x0000000000000000000000000000000000000000000000000000000000000000"); + /// assert_eq!(Seth::namehash("eth")?, "0x93cdeb708b7545dc668eb9280176169d1c33cfd8ed6f04690a0bcc88a93fc4ae"); + /// assert_eq!(Seth::namehash("foo.eth")?, "0xde9b09fd7c5f901e23a3f19fecc54828e9c848539801e86591bd9801b019f84f"); + /// assert_eq!(Seth::namehash("sub.foo.eth")?, "0x500d86f9e663479e5aaa6e99276e55fc139c597211ee47d17e1e92da16a83402"); + /// + /// Ok(()) + /// } + /// ``` + pub fn namehash(ens: &str) -> Result { + let mut node = vec![0u8; 32]; + + if !ens.is_empty() { + let ens_lower = ens.to_lowercase(); + let mut labels: Vec<&str> = ens_lower.split('.').collect(); + labels.reverse(); + + for label in labels { + let mut label_hash = keccak256(label.as_bytes()); + node.append(&mut label_hash.to_vec()); + + label_hash = keccak256(node.as_slice()); + node = label_hash.to_vec(); + } + } + + let namehash: String = node.to_hex(); + Ok(format!("0x{}", namehash)) + } } fn strip_0x(s: &str) -> &str {