diff --git a/Cargo.lock b/Cargo.lock index 9b7edf2be..224b84439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1784,7 +1784,7 @@ dependencies = [ [[package]] name = "parity-ui-precompiled" version = "1.4.0" -source = "git+https://github.com/paritytech/js-precompiled.git#69ff5bc9fa4c64a0657308a64a45a95b3d984950" +source = "git+https://github.com/paritytech/js-precompiled.git#6597fc70499226546fdcb35e7c09f9347f4f3c07" dependencies = [ "parity-dapps-glue 1.7.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/ethcore/src/blockchain/blockchain.rs b/ethcore/src/blockchain/blockchain.rs index 631369e87..2690564e5 100644 --- a/ethcore/src/blockchain/blockchain.rs +++ b/ethcore/src/blockchain/blockchain.rs @@ -1564,7 +1564,7 @@ mod tests { } fn secret() -> Secret { - Secret::from_slice(&"".sha3()).unwrap() + "".sha3().into() } #[test] diff --git a/ethcore/src/engines/authority_round.rs b/ethcore/src/engines/authority_round.rs index 75a8d58a9..b1e5a6b12 100644 --- a/ethcore/src/engines/authority_round.rs +++ b/ethcore/src/engines/authority_round.rs @@ -39,7 +39,6 @@ use super::signer::EngineSigner; use super::validator_set::{ValidatorSet, SimpleList, new_validator_set}; /// `AuthorityRound` params. -#[derive(Debug, PartialEq)] pub struct AuthorityRoundParams { /// Gas limit divisor. pub gas_limit_bound_divisor: U256, @@ -52,7 +51,7 @@ pub struct AuthorityRoundParams { /// Starting step, pub start_step: Option, /// Valid validators. - pub validators: ethjson::spec::ValidatorSet, + pub validators: Box, /// Chain score validation transition block. pub validate_score_transition: u64, /// Number of first block where EIP-155 rules are validated. @@ -66,7 +65,7 @@ impl From for AuthorityRoundParams { AuthorityRoundParams { gas_limit_bound_divisor: p.gas_limit_bound_divisor.into(), step_duration: Duration::from_secs(p.step_duration.into()), - validators: p.validators, + validators: new_validator_set(p.validators), block_reward: p.block_reward.map_or_else(U256::zero, Into::into), registrar: p.registrar.map_or_else(Address::new, Into::into), start_step: p.start_step.map(Into::into), @@ -209,7 +208,7 @@ impl AuthorityRound { proposed: AtomicBool::new(false), client: RwLock::new(None), signer: Default::default(), - validators: new_validator_set(our_params.validators), + validators: our_params.validators, validate_score_transition: our_params.validate_score_transition, eip155_transition: our_params.eip155_transition, validate_step_transition: our_params.validate_step_transition, @@ -382,14 +381,22 @@ impl Engine for AuthorityRound { return Err(From::from(BlockError::RidiculousNumber(OutOfBounds { min: Some(1), max: None, found: header.number() }))); } - // Ensure header is from the step after parent. let parent_step = header_step(parent)?; + // Ensure header is from the step after parent. if step == parent_step || (header.number() >= self.validate_step_transition && step <= parent_step) { trace!(target: "engine", "Multiple blocks proposed for step {}.", parent_step); self.validators.report_malicious(header.author(), header.number(), Default::default()); Err(EngineError::DoubleVote(header.author().clone()))?; } + // Report skipped primaries. + if step > parent_step + 1 { + for s in parent_step + 1..step { + let skipped_primary = self.step_proposer(&parent.hash(), s); + trace!(target: "engine", "Author {} did not build his block on top of the intermediate designated primary {}.", header.author(), skipped_primary); + self.validators.report_benign(&skipped_primary, header.number()); + } + } let gas_limit_divisor = self.gas_limit_bound_divisor; let min_gas = parent.gas_limit().clone() - parent.gas_limit().clone() / gas_limit_divisor; @@ -460,16 +467,18 @@ impl Engine for AuthorityRound { #[cfg(test)] mod tests { + use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; use util::*; use header::Header; use error::{Error, BlockError}; - use ethkey::Secret; use rlp::encode; use block::*; use tests::helpers::*; use account_provider::AccountProvider; use spec::Spec; - use engines::Seal; + use engines::{Seal, Engine}; + use engines::validator_set::TestSet; + use super::{AuthorityRoundParams, AuthorityRound}; #[test] fn has_valid_metadata() { @@ -513,8 +522,8 @@ mod tests { #[test] fn generates_seal_and_does_not_double_propose() { let tap = Arc::new(AccountProvider::transient_provider()); - let addr1 = tap.insert_account(Secret::from_slice(&"1".sha3()).unwrap(), "1").unwrap(); - let addr2 = tap.insert_account(Secret::from_slice(&"2".sha3()).unwrap(), "2").unwrap(); + let addr1 = tap.insert_account("1".sha3().into(), "1").unwrap(); + let addr2 = tap.insert_account("2".sha3().into(), "2").unwrap(); let spec = Spec::new_test_round(); let engine = &*spec.engine; @@ -545,7 +554,7 @@ mod tests { #[test] fn proposer_switching() { let tap = AccountProvider::transient_provider(); - let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap(); + let addr = tap.insert_account("0".sha3().into(), "0").unwrap(); let mut parent_header: Header = Header::default(); parent_header.set_seal(vec![encode(&0usize).to_vec()]); parent_header.set_gas_limit(U256::from_str("222222").unwrap()); @@ -570,7 +579,7 @@ mod tests { #[test] fn rejects_future_block() { let tap = AccountProvider::transient_provider(); - let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap(); + let addr = tap.insert_account("0".sha3().into(), "0").unwrap(); let mut parent_header: Header = Header::default(); parent_header.set_seal(vec![encode(&0usize).to_vec()]); @@ -596,7 +605,7 @@ mod tests { #[test] fn rejects_step_backwards() { let tap = AccountProvider::transient_provider(); - let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap(); + let addr = tap.insert_account("0".sha3().into(), "0").unwrap(); let mut parent_header: Header = Header::default(); parent_header.set_seal(vec![encode(&4usize).to_vec()]); @@ -616,4 +625,32 @@ mod tests { header.set_seal(vec![encode(&3usize).to_vec(), encode(&(&*signature as &[u8])).to_vec()]); assert!(engine.verify_block_family(&header, &parent_header, None).is_err()); } + + #[test] + fn reports_skipped() { + let last_benign = Arc::new(AtomicUsize::new(0)); + let params = AuthorityRoundParams { + gas_limit_bound_divisor: U256::from_str("400").unwrap(), + step_duration: Default::default(), + block_reward: Default::default(), + registrar: Default::default(), + start_step: Some(1), + validators: Box::new(TestSet::new(Default::default(), last_benign.clone())), + validate_score_transition: 0, + validate_step_transition: 0, + eip155_transition: 0, + }; + let aura = AuthorityRound::new(Default::default(), params, Default::default()).unwrap(); + + let mut parent_header: Header = Header::default(); + parent_header.set_seal(vec![encode(&1usize).to_vec()]); + parent_header.set_gas_limit(U256::from_str("222222").unwrap()); + let mut header: Header = Header::default(); + header.set_number(1); + header.set_gas_limit(U256::from_str("222222").unwrap()); + header.set_seal(vec![encode(&3usize).to_vec()]); + + assert!(aura.verify_block_family(&header, &parent_header, None).is_ok()); + assert_eq!(last_benign.load(AtomicOrdering::SeqCst), 1); + } } diff --git a/ethcore/src/engines/basic_authority.rs b/ethcore/src/engines/basic_authority.rs index 81a734f04..008a63424 100644 --- a/ethcore/src/engines/basic_authority.rs +++ b/ethcore/src/engines/basic_authority.rs @@ -229,7 +229,6 @@ mod tests { use error::{BlockError, Error}; use tests::helpers::*; use account_provider::AccountProvider; - use ethkey::Secret; use header::Header; use spec::Spec; use engines::Seal; @@ -281,7 +280,7 @@ mod tests { #[test] fn can_generate_seal() { let tap = AccountProvider::transient_provider(); - let addr = tap.insert_account(Secret::from_slice(&"".sha3()).unwrap(), "").unwrap(); + let addr = tap.insert_account("".sha3().into(), "").unwrap(); let spec = new_test_authority(); let engine = &*spec.engine; @@ -299,7 +298,7 @@ mod tests { #[test] fn seals_internally() { let tap = AccountProvider::transient_provider(); - let authority = tap.insert_account(Secret::from_slice(&"".sha3()).unwrap(), "").unwrap(); + let authority = tap.insert_account("".sha3().into(), "").unwrap(); let engine = new_test_authority().engine; assert!(!engine.seals_internally().unwrap()); diff --git a/ethcore/src/engines/tendermint/message.rs b/ethcore/src/engines/tendermint/message.rs index 304aa2671..8c2b44325 100644 --- a/ethcore/src/engines/tendermint/message.rs +++ b/ethcore/src/engines/tendermint/message.rs @@ -200,7 +200,6 @@ pub fn message_hash(vote_step: VoteStep, block_hash: H256) -> H256 { mod tests { use util::*; use rlp::*; - use ethkey::Secret; use account_provider::AccountProvider; use header::Header; use super::super::Step; @@ -250,7 +249,7 @@ mod tests { #[test] fn generate_and_verify() { let tap = Arc::new(AccountProvider::transient_provider()); - let addr = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "0").unwrap(); + let addr = tap.insert_account("0".sha3().into(), "0").unwrap(); tap.unlock_account_permanently(addr, "0".into()).unwrap(); let mi = message_info_rlp(&VoteStep::new(123, 2, Step::Precommit), Some(H256::default())); diff --git a/ethcore/src/engines/tendermint/mod.rs b/ethcore/src/engines/tendermint/mod.rs index 137f70a7a..87bc06152 100644 --- a/ethcore/src/engines/tendermint/mod.rs +++ b/ethcore/src/engines/tendermint/mod.rs @@ -42,7 +42,7 @@ use evm::Schedule; use state::CleanupMode; use io::IoService; use super::signer::EngineSigner; -use super::validator_set::{ValidatorSet, new_validator_set}; +use super::validator_set::ValidatorSet; use super::transition::TransitionHandler; use super::vote_collector::VoteCollector; use self::message::*; @@ -124,7 +124,7 @@ impl Tendermint { proposal: RwLock::new(None), proposal_parent: Default::default(), last_proposed: Default::default(), - validators: new_validator_set(our_params.validators), + validators: our_params.validators, }); let handler = TransitionHandler::new(Arc::downgrade(&engine) as Weak, Box::new(our_params.timeouts)); engine.step_service.register_handler(Arc::new(handler))?; @@ -659,7 +659,6 @@ mod tests { use block::*; use error::{Error, BlockError}; use header::Header; - use ethkey::Secret; use client::chain_notify::ChainNotify; use miner::MinerService; use tests::helpers::*; @@ -708,7 +707,7 @@ mod tests { } fn insert_and_unlock(tap: &Arc, acc: &str) -> Address { - let addr = tap.insert_account(Secret::from_slice(&acc.sha3()).unwrap(), acc).unwrap(); + let addr = tap.insert_account(acc.sha3().into(), acc).unwrap(); tap.unlock_account_permanently(addr, acc.into()).unwrap(); addr } diff --git a/ethcore/src/engines/tendermint/params.rs b/ethcore/src/engines/tendermint/params.rs index 3dbdf4041..34c743c49 100644 --- a/ethcore/src/engines/tendermint/params.rs +++ b/ethcore/src/engines/tendermint/params.rs @@ -19,16 +19,16 @@ use ethjson; use util::{U256, Uint, Address}; use time::Duration; +use super::super::validator_set::{ValidatorSet, new_validator_set}; use super::super::transition::Timeouts; use super::Step; /// `Tendermint` params. -#[derive(Debug)] pub struct TendermintParams { /// Gas limit divisor. pub gas_limit_bound_divisor: U256, /// List of validators. - pub validators: ethjson::spec::ValidatorSet, + pub validators: Box, /// Timeout durations for different steps. pub timeouts: TendermintTimeouts, /// Block reward. @@ -82,7 +82,7 @@ impl From for TendermintParams { let dt = TendermintTimeouts::default(); TendermintParams { gas_limit_bound_divisor: p.gas_limit_bound_divisor.into(), - validators: p.validators, + validators: new_validator_set(p.validators), timeouts: TendermintTimeouts { propose: p.timeout_propose.map_or(dt.propose, to_duration), prevote: p.timeout_prevote.map_or(dt.prevote, to_duration), diff --git a/ethcore/src/engines/validator_set/contract.rs b/ethcore/src/engines/validator_set/contract.rs index dd4623023..b3c06aed1 100644 --- a/ethcore/src/engines/validator_set/contract.rs +++ b/ethcore/src/engines/validator_set/contract.rs @@ -116,7 +116,6 @@ impl ValidatorSet for ValidatorContract { mod tests { use util::*; use rlp::encode; - use ethkey::Secret; use spec::Spec; use header::Header; use account_provider::AccountProvider; @@ -140,7 +139,7 @@ mod tests { #[test] fn reports_validators() { let tap = Arc::new(AccountProvider::transient_provider()); - let v1 = tap.insert_account(Secret::from_slice(&"1".sha3()).unwrap(), "").unwrap(); + let v1 = tap.insert_account("1".sha3().into(), "").unwrap(); let client = generate_dummy_client_with_spec_and_accounts(Spec::new_validator_contract, Some(tap.clone())); client.engine().register_client(Arc::downgrade(&client)); let validator_contract = Address::from_str("0000000000000000000000000000000000000005").unwrap(); diff --git a/ethcore/src/engines/validator_set/mod.rs b/ethcore/src/engines/validator_set/mod.rs index f0765db5d..622793a73 100644 --- a/ethcore/src/engines/validator_set/mod.rs +++ b/ethcore/src/engines/validator_set/mod.rs @@ -16,6 +16,8 @@ /// Validator lists. +#[cfg(test)] +mod test; mod simple_list; mod safe_contract; mod contract; @@ -28,6 +30,8 @@ use ethjson::spec::ValidatorSet as ValidatorSpec; use client::Client; use header::{Header, BlockNumber}; +#[cfg(test)] +pub use self::test::TestSet; pub use self::simple_list::SimpleList; use self::contract::ValidatorContract; use self::safe_contract::ValidatorSafeContract; diff --git a/ethcore/src/engines/validator_set/multi.rs b/ethcore/src/engines/validator_set/multi.rs index c16a3424f..27570ed27 100644 --- a/ethcore/src/engines/validator_set/multi.rs +++ b/ethcore/src/engines/validator_set/multi.rs @@ -166,9 +166,9 @@ mod tests { fn uses_current_set() { ::env_logger::init().unwrap(); let tap = Arc::new(AccountProvider::transient_provider()); - let s0 = Secret::from_slice(&"0".sha3()).unwrap(); + let s0: Secret = "0".sha3().into(); let v0 = tap.insert_account(s0.clone(), "").unwrap(); - let v1 = tap.insert_account(Secret::from_slice(&"1".sha3()).unwrap(), "").unwrap(); + let v1 = tap.insert_account("1".sha3().into(), "").unwrap(); let client = generate_dummy_client_with_spec_and_accounts(Spec::new_validator_multi, Some(tap)); client.engine().register_client(Arc::downgrade(&client)); diff --git a/ethcore/src/engines/validator_set/safe_contract.rs b/ethcore/src/engines/validator_set/safe_contract.rs index 262aa0def..27415418e 100644 --- a/ethcore/src/engines/validator_set/safe_contract.rs +++ b/ethcore/src/engines/validator_set/safe_contract.rs @@ -294,9 +294,9 @@ mod tests { #[test] fn knows_validators() { let tap = Arc::new(AccountProvider::transient_provider()); - let s0 = Secret::from_slice(&"1".sha3()).unwrap(); + let s0: Secret = "1".sha3().into(); let v0 = tap.insert_account(s0.clone(), "").unwrap(); - let v1 = tap.insert_account(Secret::from_slice(&"0".sha3()).unwrap(), "").unwrap(); + let v1 = tap.insert_account("0".sha3().into(), "").unwrap(); let network_id = Spec::new_validator_safe_contract().network_id(); let client = generate_dummy_client_with_spec_and_accounts(Spec::new_validator_safe_contract, Some(tap)); client.engine().register_client(Arc::downgrade(&client)); diff --git a/ethcore/src/engines/validator_set/test.rs b/ethcore/src/engines/validator_set/test.rs new file mode 100644 index 000000000..f1d0e76cd --- /dev/null +++ b/ethcore/src/engines/validator_set/test.rs @@ -0,0 +1,88 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +/// Used for Engine testing. + +use std::str::FromStr; +use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering}; +use util::{Arc, Bytes, H256, Address, HeapSizeOf}; + +use engines::Call; +use header::{Header, BlockNumber}; +use super::{ValidatorSet, SimpleList}; + +/// Set used for testing with a single validator. +pub struct TestSet { + validator: SimpleList, + last_malicious: Arc, + last_benign: Arc, +} + +impl TestSet { + pub fn new(last_malicious: Arc, last_benign: Arc) -> Self { + TestSet { + validator: SimpleList::new(vec![Address::from_str("7d577a597b2742b498cb5cf0c26cdcd726d39e6e").unwrap()]), + last_malicious: last_malicious, + last_benign: last_benign, + } + } +} + +impl HeapSizeOf for TestSet { + fn heap_size_of_children(&self) -> usize { + self.validator.heap_size_of_children() + } +} + +impl ValidatorSet for TestSet { + fn default_caller(&self, _block_id: ::ids::BlockId) -> Box { + Box::new(|_, _| Err("Test set doesn't require calls.".into())) + } + + fn is_epoch_end(&self, _header: &Header, _block: Option<&[u8]>, _receipts: Option<&[::receipt::Receipt]>) + -> ::engines::EpochChange + { + ::engines::EpochChange::No + } + + fn epoch_proof(&self, _header: &Header, _caller: &Call) -> Result, String> { + Ok(Vec::new()) + } + + fn epoch_set(&self, _header: &Header, _: &[u8]) -> Result<(u64, SimpleList), ::error::Error> { + Ok((0, self.validator.clone())) + } + + fn contains_with_caller(&self, bh: &H256, address: &Address, _: &Call) -> bool { + self.validator.contains(bh, address) + } + + fn get_with_caller(&self, bh: &H256, nonce: usize, _: &Call) -> Address { + self.validator.get(bh, nonce) + } + + fn count_with_caller(&self, _bh: &H256, _: &Call) -> usize { + 1 + } + + fn report_malicious(&self, _validator: &Address, block: BlockNumber, _proof: Bytes) { + self.last_malicious.store(block as usize, AtomicOrdering::SeqCst) + } + + fn report_benign(&self, _validator: &Address, block: BlockNumber) { + self.last_benign.store(block as usize, AtomicOrdering::SeqCst) + } +} diff --git a/ethcore/src/state/mod.rs b/ethcore/src/state/mod.rs index 90055f275..384c0cca9 100644 --- a/ethcore/src/state/mod.rs +++ b/ethcore/src/state/mod.rs @@ -962,7 +962,7 @@ mod tests { use types::executed::CallType; fn secret() -> Secret { - Secret::from_slice(&"".sha3()).unwrap() + "".sha3().into() } #[test] diff --git a/ethcore/src/tests/client.rs b/ethcore/src/tests/client.rs index 02db15617..b86c84a07 100644 --- a/ethcore/src/tests/client.rs +++ b/ethcore/src/tests/client.rs @@ -27,7 +27,7 @@ use devtools::*; use miner::Miner; use spec::Spec; use views::BlockView; -use ethkey::{KeyPair, Secret}; +use ethkey::KeyPair; use transaction::{PendingTransaction, Transaction, Action, Condition}; use miner::MinerService; @@ -296,7 +296,7 @@ fn change_history_size() { #[test] fn does_not_propagate_delayed_transactions() { - let key = KeyPair::from_secret(Secret::from_slice(&"test".sha3()).unwrap()).unwrap(); + let key = KeyPair::from_secret("test".sha3().into()).unwrap(); let secret = key.secret(); let tx0 = PendingTransaction::new(Transaction { nonce: 0.into(), diff --git a/ethcore/src/types/transaction.rs b/ethcore/src/types/transaction.rs index 2c35400a8..2b17cee25 100644 --- a/ethcore/src/types/transaction.rs +++ b/ethcore/src/types/transaction.rs @@ -113,7 +113,7 @@ impl HeapSizeOf for Transaction { impl From for SignedTransaction { fn from(t: ethjson::state::Transaction) -> Self { let to: Option = t.to.into(); - let secret = t.secret.map(|s| Secret::from_slice(&s.0).expect("Valid secret expected.")); + let secret = t.secret.map(|s| Secret::from_slice(&s.0)); let tx = Transaction { nonce: t.nonce.into(), gas_price: t.gas_price.into(), diff --git a/ethcrypto/src/lib.rs b/ethcrypto/src/lib.rs index 4f14cf4d9..209162bba 100644 --- a/ethcrypto/src/lib.rs +++ b/ethcrypto/src/lib.rs @@ -192,7 +192,7 @@ pub mod ecdh { let sec = key::SecretKey::from_slice(context, &secret)?; let shared = ecdh::SharedSecret::new_raw(context, &publ, &sec); - Secret::from_slice(&shared[0..32]) + Secret::from_unsafe_slice(&shared[0..32]) .map_err(|_| Error::Secp(SecpError::InvalidSecretKey)) } } diff --git a/ethkey/src/brain.rs b/ethkey/src/brain.rs index cf675a843..9976bdb01 100644 --- a/ethkey/src/brain.rs +++ b/ethkey/src/brain.rs @@ -38,7 +38,7 @@ impl Generator for Brain { match i > 16384 { false => i += 1, true => { - if let Ok(secret) = Secret::from_slice(&secret) { + if let Ok(secret) = Secret::from_unsafe_slice(&secret) { let result = KeyPair::from_secret(secret); if result.as_ref().ok().map_or(false, |r| r.address()[0] == 0) { return result; diff --git a/ethkey/src/extended.rs b/ethkey/src/extended.rs index 77b35f774..7df6fde1c 100644 --- a/ethkey/src/extended.rs +++ b/ethkey/src/extended.rs @@ -99,8 +99,7 @@ impl ExtendedSecret { pub fn derive(&self, index: Derivation) -> ExtendedSecret where T: Label { let (derived_key, next_chain_code) = derivation::private(*self.secret, self.chain_code, index); - let derived_secret = Secret::from_slice(&*derived_key) - .expect("Derivation always produced a valid private key; qed"); + let derived_secret = Secret::from_slice(&*derived_key); ExtendedSecret::with_code(derived_secret, next_chain_code) } @@ -181,7 +180,7 @@ impl ExtendedKeyPair { pub fn with_seed(seed: &[u8]) -> Result { let (master_key, chain_code) = derivation::seed_pair(seed); Ok(ExtendedKeyPair::with_secret( - Secret::from_slice(&*master_key).map_err(|_| DerivationError::InvalidSeed)?, + Secret::from_unsafe_slice(&*master_key).map_err(|_| DerivationError::InvalidSeed)?, chain_code, )) } @@ -402,7 +401,7 @@ mod tests { fn test_extended(f: F, test_private: H256) where F: Fn(ExtendedSecret) -> ExtendedSecret { let (private_seed, chain_code) = master_chain_basic(); - let extended_secret = ExtendedSecret::with_code(Secret::from_slice(&*private_seed).unwrap(), chain_code); + let extended_secret = ExtendedSecret::with_code(Secret::from_slice(&*private_seed), chain_code); let derived = f(extended_secret); assert_eq!(**derived.as_raw(), test_private); } diff --git a/ethkey/src/keypair.rs b/ethkey/src/keypair.rs index 58747e172..8975063d5 100644 --- a/ethkey/src/keypair.rs +++ b/ethkey/src/keypair.rs @@ -62,7 +62,7 @@ impl KeyPair { } pub fn from_secret_slice(slice: &[u8]) -> Result { - Self::from_secret(Secret::from_slice(slice)?) + Self::from_secret(Secret::from_unsafe_slice(slice)?) } pub fn from_keypair(sec: key::SecretKey, publ: key::PublicKey) -> Self { diff --git a/ethkey/src/secret.rs b/ethkey/src/secret.rs index e1c5bd2fa..de35d6b04 100644 --- a/ethkey/src/secret.rs +++ b/ethkey/src/secret.rs @@ -33,7 +33,7 @@ impl fmt::Debug for Secret { } impl Secret { - fn from_slice_unchecked(key: &[u8]) -> Self { + pub fn from_slice(key: &[u8]) -> Self { assert_eq!(32, key.len(), "Caller should provide 32-byte length slice"); let mut h = H256::default(); @@ -41,11 +41,17 @@ impl Secret { Secret { inner: h } } - pub fn from_slice(key: &[u8]) -> Result { + /// Imports and validates the key. + pub fn from_unsafe_slice(key: &[u8]) -> Result { let secret = key::SecretKey::from_slice(&super::SECP256K1, key)?; Ok(secret.into()) } + /// Checks validity of this key. + pub fn check_validity(&self) -> Result<(), Error> { + self.to_secp256k1_secret().map(|_| ()) + } + /// Inplace add one secret key to another (scalar + scalar) pub fn add(&mut self, other: &Secret) -> Result<(), Error> { let mut key_secret = self.to_secp256k1_secret()?; @@ -121,14 +127,25 @@ impl Secret { impl FromStr for Secret { type Err = Error; fn from_str(s: &str) -> Result { - let hash = H256::from_str(s).map_err(|e| Error::Custom(format!("{:?}", e)))?; - Self::from_slice(&hash) + Ok(H256::from_str(s).map_err(|e| Error::Custom(format!("{:?}", e)))?.into()) + } +} + +impl From for Secret { + fn from(s: H256) -> Self { + Secret::from_slice(&s) + } +} + +impl From<&'static str> for Secret { + fn from(s: &'static str) -> Self { + s.parse().expect(&format!("invalid string literal for {}: '{}'", stringify!(Self), s)) } } impl From for Secret { fn from(key: key::SecretKey) -> Self { - Self::from_slice_unchecked(&key[0..32]) + Self::from_slice(&key[0..32]) } } diff --git a/ethstore/src/account/crypto.rs b/ethstore/src/account/crypto.rs index 343e44cb4..9d1530429 100755 --- a/ethstore/src/account/crypto.rs +++ b/ethstore/src/account/crypto.rs @@ -122,7 +122,7 @@ impl Crypto { } let secret = self.do_decrypt(password, 32)?; - Ok(Secret::from_slice(&secret)?) + Ok(Secret::from_unsafe_slice(&secret)?) } /// Try to decrypt and return result as is diff --git a/ethstore/src/dir/mod.rs b/ethstore/src/dir/mod.rs index fb22c06ee..b6e168f6e 100755 --- a/ethstore/src/dir/mod.rs +++ b/ethstore/src/dir/mod.rs @@ -90,7 +90,7 @@ pub trait VaultKeyDirectory: KeyDirectory { fn set_meta(&self, meta: &str) -> Result<(), Error>; } -pub use self::disk::RootDiskDirectory; +pub use self::disk::{RootDiskDirectory, DiskKeyFileManager, KeyFileManager}; pub use self::memory::MemoryDirectory; pub use self::vault::VaultDiskDirectory; diff --git a/ethstore/src/import.rs b/ethstore/src/import.rs index b7497c9ff..2b2c34eb1 100644 --- a/ethstore/src/import.rs +++ b/ethstore/src/import.rs @@ -15,10 +15,29 @@ // along with Parity. If not, see . use std::collections::HashSet; +use std::path::Path; +use std::fs; + use ethkey::Address; -use dir::{paths, KeyDirectory, RootDiskDirectory}; +use dir::{paths, KeyDirectory, RootDiskDirectory, DiskKeyFileManager, KeyFileManager}; use Error; +/// Import an account from a file. +pub fn import_account(path: &Path, dst: &KeyDirectory) -> Result { + let key_manager = DiskKeyFileManager; + let existing_accounts = dst.load()?.into_iter().map(|a| a.address).collect::>(); + let filename = path.file_name().and_then(|n| n.to_str()).map(|f| f.to_owned()); + let account = fs::File::open(&path) + .map_err(Into::into) + .and_then(|file| key_manager.read(filename, file))?; + + let address = account.address.clone(); + if !existing_accounts.contains(&address) { + dst.insert(account)?; + } + Ok(address) +} + /// Import all accounts from one directory to the other. pub fn import_accounts(src: &KeyDirectory, dst: &KeyDirectory) -> Result, Error> { let accounts = src.load()?; diff --git a/ethstore/src/lib.rs b/ethstore/src/lib.rs index 145a58023..00a06a3e5 100755 --- a/ethstore/src/lib.rs +++ b/ethstore/src/lib.rs @@ -57,7 +57,7 @@ mod secret_store; pub use self::account::{SafeAccount, Crypto}; pub use self::error::Error; pub use self::ethstore::{EthStore, EthMultiStore}; -pub use self::import::{import_accounts, read_geth_accounts}; +pub use self::import::{import_account, import_accounts, read_geth_accounts}; pub use self::json::OpaqueKeyFile as KeyFile; pub use self::presale::PresaleWallet; pub use self::secret_store::{ diff --git a/ethstore/src/presale.rs b/ethstore/src/presale.rs index dbbdcdc8d..0c3e72e31 100644 --- a/ethstore/src/presale.rs +++ b/ethstore/src/presale.rs @@ -50,7 +50,7 @@ impl PresaleWallet { let len = crypto::aes::decrypt_cbc(&derived_key, &self.iv, &self.ciphertext, &mut key).map_err(|_| Error::InvalidPassword)?; let unpadded = &key[..len]; - let secret = Secret::from_slice(&unpadded.keccak256())?; + let secret = Secret::from_unsafe_slice(&unpadded.keccak256())?; if let Ok(kp) = KeyPair::from_secret(secret) { if kp.address() == self.address { return Ok(kp) diff --git a/js/.babelrc b/js/.babelrc index 5087af80d..127abf143 100644 --- a/js/.babelrc +++ b/js/.babelrc @@ -7,6 +7,8 @@ "transform-decorators-legacy", "transform-class-properties", "transform-object-rest-spread", + "transform-es2015-modules-commonjs", + "transform-runtime", "lodash", "recharts" ], @@ -25,7 +27,6 @@ }, "test": { "plugins": [ - "transform-runtime", [ "babel-plugin-webpack-alias", { "config": "webpack/test.js" } ] ] } diff --git a/js/package.json b/js/package.json index 07cf72dac..bb7300548 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,6 @@ { "name": "parity.js", - "version": "1.7.80", + "version": "1.7.82", "main": "release/index.js", "jsnext:main": "src/index.js", "author": "Parity Team ", @@ -77,11 +77,11 @@ "babel-plugin-recharts": "1.1.0", "babel-plugin-transform-class-properties": "6.23.0", "babel-plugin-transform-decorators-legacy": "1.3.4", + "babel-plugin-transform-es2015-modules-commonjs": "6.24.1", "babel-plugin-transform-object-rest-spread": "6.23.0", "babel-plugin-transform-react-remove-prop-types": "0.3.2", "babel-plugin-transform-runtime": "6.23.0", "babel-plugin-webpack-alias": "2.1.2", - "babel-polyfill": "6.23.0", "babel-preset-env": "1.1.9", "babel-preset-es2015": "6.22.0", "babel-preset-es2016": "6.22.0", @@ -183,6 +183,7 @@ "promise-worker": "1.1.1", "react": "15.4.2", "react-dom": "15.4.2", + "react-inspector": "paritytech/react-inspector", "react-intl": "2.1.5", "react-router": "3.0.0", "react-router-redux": "4.0.7", diff --git a/js/src/api/api.js b/js/src/api/api.js index 526ecbf1b..0b6e444c8 100644 --- a/js/src/api/api.js +++ b/js/src/api/api.js @@ -23,7 +23,7 @@ import { Db, Eth, Parity, Net, Personal, Shh, Signer, Trace, Web3 } from './rpc' import Subscriptions from './subscriptions'; import util from './util'; import { isFunction } from './util/types'; -import { LocalAccountsMiddleware } from './local'; +// import { LocalAccountsMiddleware } from './local'; export default class Api extends EventEmitter { constructor (transport, allowSubscriptions = true) { @@ -54,9 +54,9 @@ export default class Api extends EventEmitter { const middleware = this.parity .nodeKind() .then((nodeKind) => { - if (nodeKind.availability === 'public') { - return LocalAccountsMiddleware; - } + // if (nodeKind.availability === 'public') { + // return LocalAccountsMiddleware; + // } return null; }) diff --git a/js/src/dapps/console.js b/js/src/dapps/console.js new file mode 100644 index 000000000..44b6dcb9c --- /dev/null +++ b/js/src/dapps/console.js @@ -0,0 +1,59 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import React from 'react'; +import ReactDOM from 'react-dom'; +import { AppContainer } from 'react-hot-loader'; + +import 'codemirror/addon/dialog/dialog'; +import 'codemirror/addon/dialog/dialog.css'; +import 'codemirror/addon/hint/javascript-hint'; +import 'codemirror/addon/hint/show-hint'; +import 'codemirror/addon/hint/show-hint.css'; +import 'codemirror/addon/search/match-highlighter'; +import 'codemirror/addon/search/search'; +import 'codemirror/addon/search/searchcursor'; +import 'codemirror/keymap/sublime'; +import 'codemirror/lib/codemirror.css'; +import 'codemirror/mode/javascript/javascript'; +// Custom codemirror style +import './console/codemirror.css'; + +import Application from './console/Application'; + +import '../../assets/fonts/Roboto/font.css'; +import '../../assets/fonts/RobotoMono/font.css'; +import './style.css'; + +ReactDOM.render( + + + , + document.querySelector('#container') +); + +if (module.hot) { + module.hot.accept('./console/Application/index.js', () => { + require('./console/Application/index.js'); + + ReactDOM.render( + + + , + document.querySelector('#container') + ); + }); +} diff --git a/js/src/dapps/console/Application/application.css b/js/src/dapps/console/Application/application.css new file mode 100644 index 000000000..eea2c030d --- /dev/null +++ b/js/src/dapps/console/Application/application.css @@ -0,0 +1,65 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.app { + display: flex; + flex-direction: column; + font-family: Arial, sans-serif; + font-size: 11px; + height: 100vh; + overflow: hidden; +} + +textarea, +input { + font-family: dejavu sans mono, monospace; + outline: none; +} + +code, +pre { + font-family: dejavu sans mono, monospace; + font-size: 11px; +} + +.header { + flex: 0 0 auto; +} + +.view { + display: flex; + flex: 1; + flex-direction: column; +} + +.eval { + flex: 0 1 auto; + font-family: dejavu sans mono, monospace; + overflow: auto; +} + +.input { + border-top: 1px solid #eee; + display: flex; + flex: 1 1 auto; + min-height: 50px; +} + +.status { + flex: 0 0 auto; + font-family: dejavu sans mono, monospace; +} diff --git a/js/src/dapps/console/Application/application.js b/js/src/dapps/console/Application/application.js new file mode 100644 index 000000000..5a591e710 --- /dev/null +++ b/js/src/dapps/console/Application/application.js @@ -0,0 +1,94 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { observer } from 'mobx-react'; +import React, { Component } from 'react'; + +import { api } from '../parity'; + +import Console from '../Console'; +import Header from '../Header'; +import Input from '../Input'; +import Settings from '../Settings'; +import Snippets from '../Snippets'; +import Watches from '../Watches'; + +import ApplicationStore from './application.store'; +import WatchesStore from '../Watches/watches.store'; + +import styles from './application.css'; + +@observer +export default class Application extends Component { + application = ApplicationStore.get(); + watches = WatchesStore.get(); + + componentWillMount () { + this.watches.add('time', () => new Date()); + this.watches.add('blockNumber', api.eth.blockNumber, api); + } + + render () { + return ( +
+
+
+
+ + { this.renderView() } + +
+ +
+
+ ); + } + + renderView () { + const { view } = this.application; + + if (view === 'console') { + return ( +
+
+ +
+
+ +
+
+ ); + } + + if (view === 'settings') { + return ( +
+ +
+ ); + } + + if (view === 'snippets') { + return ( +
+ +
+ ); + } + + return null; + } +} diff --git a/js/src/dapps/console/Application/application.store.js b/js/src/dapps/console/Application/application.store.js new file mode 100644 index 000000000..c10be46c6 --- /dev/null +++ b/js/src/dapps/console/Application/application.store.js @@ -0,0 +1,42 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { action, observable } from 'mobx'; + +let instance; + +export default class ApplicationStore { + @observable view = this.views[0].id; + + views = [ + { label: 'Console', id: 'console' }, + { label: 'Snippets', id: 'snippets' }, + { label: 'Settings', id: 'settings' } + ]; + + static get () { + if (!instance) { + instance = new ApplicationStore(); + } + + return instance; + } + + @action + setView (view) { + this.view = view; + } +} diff --git a/js/src/dapps/console/Application/index.js b/js/src/dapps/console/Application/index.js new file mode 100644 index 000000000..3d8d1ca3b --- /dev/null +++ b/js/src/dapps/console/Application/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './application'; diff --git a/js/src/dapps/console/Autocomplete/autocomplete.css b/js/src/dapps/console/Autocomplete/autocomplete.css new file mode 100644 index 000000000..8d4585e7a --- /dev/null +++ b/js/src/dapps/console/Autocomplete/autocomplete.css @@ -0,0 +1,55 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.container { + background: #f8f8f8; + box-shadow: 0 0.125em 0.25em rgba(0, 0, 0, 0.5); + font-family: dejavu sans mono, monospace; + left: 20px; + position: absolute; + max-height: 300px; + overflow: auto; +} + +.item { + background-color: white; + padding: 0.25em 0.25em 0.25em 0.35em; + display: flex; + justify-content: space-between; + + &.selected { + background-color: rgb(64, 115, 244); + + &, + .proto { + color: white; + } + } + + &:hover { + cursor: default; + } + + &:hover:not(.selected) { + background-color: rgb(230, 236, 255); + } + + .proto { + color: gray; + margin-left: 1em; + } +} diff --git a/js/src/dapps/console/Autocomplete/autocomplete.js b/js/src/dapps/console/Autocomplete/autocomplete.js new file mode 100644 index 000000000..e2938f23d --- /dev/null +++ b/js/src/dapps/console/Autocomplete/autocomplete.js @@ -0,0 +1,96 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { observer } from 'mobx-react'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; + +import AutocompleteStore from './autocomplete.store'; + +import styles from './autocomplete.css'; + +@observer +export default class Autocomplete extends Component { + autocompleteStore = AutocompleteStore.get(); + + render () { + if (!this.autocompleteStore.show) { + return null; + } + + return ( +
+ { this.renderAutocompletes() } +
+ ); + } + + renderAutocompletes () { + const { selected, values } = this.autocompleteStore; + const displayedProto = {}; + + return values.map((autocomplete, index) => { + const { name, prototypeName } = autocomplete; + const onClick = () => this.handleClick(index); + const setRef = (node) => this.setRef(index, node); + + const proto = !displayedProto[prototypeName] + ? ( + + { prototypeName } + + ) + : null; + + if (!displayedProto[prototypeName]) { + displayedProto[prototypeName] = true; + } + + const classes = [ styles.item ]; + + if (index === selected) { + classes.push(styles.selected); + } + + return ( +
+ + { name } + + { proto } +
+ ); + }); + } + + handleClick = (index) => { + this.autocompleteStore.select(index); + }; + + setRef = (index, node) => { + const element = ReactDOM.findDOMNode(node); + + this.autocompleteStore.setElement(index, element); + }; +} diff --git a/js/src/dapps/console/Autocomplete/autocomplete.store.js b/js/src/dapps/console/Autocomplete/autocomplete.store.js new file mode 100644 index 000000000..82ff2f24d --- /dev/null +++ b/js/src/dapps/console/Autocomplete/autocomplete.store.js @@ -0,0 +1,234 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { action, observable } from 'mobx'; + +import { evaluate } from '../utils'; + +let instance; + +export default class AutocompleteStore { + @observable values = []; + @observable position = {}; + @observable show = false; + @observable selected = null; + + elements = {}; + inputNode = null; + lastObject = null; + lastObjectPropertyNames = []; + + static get () { + if (!instance) { + instance = new AutocompleteStore(); + } + + return instance; + } + + get hasSelected () { + return this.selected !== null; + } + + clearCache () { + this.lastObject = null; + this.lastObjectPropertyNames = null; + } + + @action + focus (offset = 1) { + if (this.values.length === 0) { + this.selected = null; + return; + } + + this.selected = this.selected === null + ? ( + offset === 1 + ? 0 + : this.values.length - 1 + ) + : (this.values.length + this.selected + offset) % (this.values.length); + + if (this.isVisible(this.selected)) { + return; + } + + const element = this.elements[this.selected]; + + if (!element) { + return; + } + + element.scrollIntoView(offset === -1); + } + + focusOnInput () { + if (!this.inputNode) { + return; + } + + this.inputNode.focus(); + } + + @action + hide () { + this.show = false; + this.selected = null; + } + + isVisible (index) { + const element = this.elements[index]; + + if (!element) { + return false; + } + + const eBoundings = element.getBoundingClientRect(); + const pBoundings = element.parentElement.getBoundingClientRect(); + + if (eBoundings.top < pBoundings.top || eBoundings.bottom > pBoundings.bottom) { + return false; + } + + return true; + } + + select (inputStore, _index = this.selected) { + const index = _index === null + ? 0 + : _index; + + if (!this.values[index]) { + console.warn(`autocomplete::select has been called on AutocompleteStore with wrong value ${index}`); + return; + } + + const { name } = this.values[index]; + const { input } = inputStore; + const objects = input.split('.'); + + objects[objects.length - 1] = name; + const nextInput = objects.join('.'); + + this.hide(); + this.focusOnInput(); + return inputStore.updateInput(nextInput, false); + } + + setElement (index, element) { + this.elements[index] = element; + } + + setInputNode (node) { + this.inputNode = node; + } + + @action + setPosition () { + if (!this.inputNode) { + return; + } + + const inputBoundings = this.inputNode.getBoundingClientRect(); + const bodyBoundings = document.body.getBoundingClientRect(); + + // display on bottom of input + if (inputBoundings.top < bodyBoundings.height / 2) { + const nextPosition = { + top: 20 + }; + + this.position = nextPosition; + return; + } + + // display on top of input + const nextPosition = { + bottom: inputBoundings.height + }; + + this.position = nextPosition; + return; + } + + @action + setValues (values) { + this.values = values; + this.selected = null; + const show = values.length > 0; + + // Reveal autocomplete + if (!this.show && show) { + this.setPosition(); + } + + this.show = show; + } + + update (input) { + if (input.length === 0) { + return this.setValues([]); + } + + const objects = input.split('.'); + const suffix = objects.pop().toLowerCase(); + const prefix = objects.join('.'); + const object = prefix.length > 0 + ? prefix + : 'window'; + + if (object !== this.lastObject) { + const evalResult = evaluate(object); + + if (evalResult.error) { + this.lastObjectProperties = []; + } else { + this.lastObjectProperties = getAllProperties(evalResult.result); + } + + this.lastObject = object; + } + + const autocompletes = this.lastObjectProperties.filter((property) => { + return property.name.toLowerCase().includes(suffix); + }); + + return this.setValues(autocompletes); + } +} + +function getAllProperties (object) { + const propertyNames = {}; + + while (object) { + const prototypeName = object && object.constructor && object.constructor.name || ''; + + Object.getOwnPropertyNames(object) + .sort() + .forEach((name) => { + if (Object.prototype.hasOwnProperty.call(propertyNames, name)) { + return; + } + + propertyNames[name] = { name, prototypeName }; + }); + + object = Object.getPrototypeOf(object); + } + + return Object.values(propertyNames); +} diff --git a/js/src/dapps/console/Autocomplete/index.js b/js/src/dapps/console/Autocomplete/index.js new file mode 100644 index 000000000..5761be0e3 --- /dev/null +++ b/js/src/dapps/console/Autocomplete/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './autocomplete'; diff --git a/js/src/dapps/console/Console/console.css b/js/src/dapps/console/Console/console.css new file mode 100644 index 000000000..a0b3db4ff --- /dev/null +++ b/js/src/dapps/console/Console/console.css @@ -0,0 +1,58 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.result { + border-top: 1px solid #eee; + display: flex; + font-family: dejavu sans mono, monospace; + padding: 0.35em 0.25em; + + &.error { + background-color: hsl(0, 100%, 97%); + + .text { + color: red; + } + } + + &.warn { + background-color: hsl(50, 100%, 95%); + } +} + +.type { + font-weight: bold !important; + font-size: 8pt; + padding: 0 0.5em 0 0.25em; +} + +.time { + color: gray; + padding: 0 1em 0 0.5em; +} + +.token { + white-space: pre-wrap; +} + +.text { + display: flex; +} + +.text .token:not(:first-child) { + margin-left: 0.5em; +} diff --git a/js/src/dapps/console/Console/console.js b/js/src/dapps/console/Console/console.js new file mode 100644 index 000000000..75f9713a6 --- /dev/null +++ b/js/src/dapps/console/Console/console.js @@ -0,0 +1,118 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { observer } from 'mobx-react'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; +import { ObjectInspector } from 'react-inspector'; + +import ConsoleStore from './console.store'; +import SettingsStore from '../Settings/settings.store'; + +import styles from './console.css'; + +const ICONS = { + debug: ' ', + error: '✖', + info: 'ℹ', + input: '>', + log: ' ', + result: '<', + warn: '⚠' +}; + +@observer +export default class Console extends Component { + consoleStore = ConsoleStore.get(); + settingsStore = SettingsStore.get(); + + render () { + return ( +
+ { this.renderResults() } +
+ ); + } + + renderResults () { + const { logs } = this.consoleStore; + + return logs.map((data, index) => { + const { type, timestamp } = data; + const values = this.consoleStore.logValues[index]; + const classes = [ styles.result, styles[type] ]; + + return ( +
+ + { this.renderTimestamp(timestamp) } + + { + values.map((value, valueIndex) => ( + + { this.toString(value) } + + )) + } + +
+ ); + }); + } + + renderTimestamp (timestamp) { + const { displayTimestamps } = this.settingsStore; + + if (!displayTimestamps) { + return null; + } + + return ( + + { new Date(timestamp).toISOString().slice(11, 23) } + + ); + } + + setRef = (node) => { + const element = ReactDOM.findDOMNode(node); + + this.consoleStore.setNode(element); + }; + + toString (value) { + if (typeof value === 'string') { + return value; + } + + if (value instanceof Error) { + return value.toString(); + } + + return ( + + ); + } +} diff --git a/js/src/dapps/console/Console/console.store.js b/js/src/dapps/console/Console/console.store.js new file mode 100644 index 000000000..dc2fc6db4 --- /dev/null +++ b/js/src/dapps/console/Console/console.store.js @@ -0,0 +1,126 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { action, observable } from 'mobx'; + +import AutocompleteStore from '../Autocomplete/autocomplete.store'; +import { evaluate } from '../utils'; + +let instance; + +export default class ConsoleStore { + @observable logs = []; + + autocompleteStore = AutocompleteStore.get(); + logValues = []; + node = null; + + constructor () { + this.attachConsole(); + } + + static get () { + if (!instance) { + instance = new ConsoleStore(); + } + + return instance; + } + + attachConsole () { + ['debug', 'error', 'info', 'log', 'warn'].forEach((level) => { + const old = window.console[level].bind(window.console); + + window.console[level] = (...args) => { + old(...args); + this.log({ type: level, values: args }); + }; + }); + } + + @action + clear () { + this.logs = []; + this.logValues = []; + } + + evaluate (input) { + this.log({ type: 'input', value: input }); + + setTimeout(() => { + const { result, error } = evaluate(input); + let value = error || result; + const type = error + ? 'error' + : 'result'; + + if (typeof value === 'string') { + value = `"${value}"`; + } + + if (value && typeof value === 'object' && typeof value.then === 'function') { + return value + .then((result) => { + this.log({ type: 'result', value: result }); + }) + .catch((error) => { + this.log({ type: 'error', value: error }); + }); + } + + this.log({ type, value }); + }); + } + + @action + log ({ type, value, values }) { + this.logs.push({ + type, + timestamp: Date.now() + }); + + if (values) { + this.logValues.push(values); + } else { + this.logValues.push([ value ]); + } + + this.autocompleteStore.setPosition(); + this.scroll(); + } + + setNode (node) { + this.node = node; + this.scroll(); + } + + scroll () { + if (!this.node) { + return; + } + + setTimeout(() => { + if (this.node.children.length === 0) { + return; + } + + // Scroll to the last child + this.node + .children[this.node.children.length - 1] + .scrollIntoView(false); + }, 50); + } +} diff --git a/js/src/dapps/console/Console/index.js b/js/src/dapps/console/Console/index.js new file mode 100644 index 000000000..2956b330f --- /dev/null +++ b/js/src/dapps/console/Console/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './console'; diff --git a/js/src/dapps/console/Header/header.css b/js/src/dapps/console/Header/header.css new file mode 100644 index 000000000..116de6b8c --- /dev/null +++ b/js/src/dapps/console/Header/header.css @@ -0,0 +1,51 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.container { + background-color: #f3f3f3; + border-bottom: 1px solid #ccc; + font-size: 12px; + padding: 0 0.5em; +} + +.tabs { + display: flex; +} + +.tab { + align-items: center; + box-sizing: border-box; + border: 1px solid transparent; + color: #333; + cursor: default; + display: flex; + height: 24px; + line-height: 15px; + margin-top: 2px; + padding: 2px 6px 2px 4px; + + &:hover, + &.active:hover { + background-color: #e5e5e5; + } + + &.active { + background-color: white; + border: 1px solid #ccc; + border-bottom: none; + } +} diff --git a/js/src/dapps/console/Header/header.js b/js/src/dapps/console/Header/header.js new file mode 100644 index 000000000..c422b8256 --- /dev/null +++ b/js/src/dapps/console/Header/header.js @@ -0,0 +1,65 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import { observer } from 'mobx-react'; +import React, { Component } from 'react'; + +import ApplicationStore from '../Application/application.store'; + +import styles from './header.css'; + +@observer +export default class Header extends Component { + application = ApplicationStore.get(); + + render () { + return ( +
+
+ { this.renderTabs() } +
+
+ ); + } + + renderTabs () { + const { view } = this.application; + + return this.application.views.map((tab) => { + const { label, id } = tab; + const classes = [ styles.tab ]; + const onClick = () => this.handleClickTab(id); + + if (id === view) { + classes.push(styles.active); + } + + return ( +
+ { label } +
+ ); + }); + } + + handleClickTab = (id) => { + this.application.setView(id); + }; +} diff --git a/js/src/dapps/console/Header/index.js b/js/src/dapps/console/Header/index.js new file mode 100644 index 000000000..aef90266f --- /dev/null +++ b/js/src/dapps/console/Header/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './header'; diff --git a/js/src/dapps/console/Input/index.js b/js/src/dapps/console/Input/index.js new file mode 100644 index 000000000..29e00f72b --- /dev/null +++ b/js/src/dapps/console/Input/index.js @@ -0,0 +1,17 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +export default from './input'; diff --git a/js/src/dapps/console/Input/input.css b/js/src/dapps/console/Input/input.css new file mode 100644 index 000000000..7b0c2306e --- /dev/null +++ b/js/src/dapps/console/Input/input.css @@ -0,0 +1,46 @@ +/* Copyright 2015-2017 Parity Technologies (UK) Ltd. +/* This file is part of Parity. +/* +/* Parity is free software: you can redistribute it and/or modify +/* it under the terms of the GNU General Public License as published by +/* the Free Software Foundation, either version 3 of the License, or +/* (at your option) any later version. +/* +/* Parity is distributed in the hope that it will be useful, +/* but WITHOUT ANY WARRANTY; without even the implied warranty of +/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +/* GNU General Public License for more details. +/* +/* You should have received a copy of the GNU General Public License +/* along with Parity. If not, see . +*/ + +.type { + color: #59f; + font-weight: bold !important; + font-size: 11px; + padding: 0 0.5em 0 0.25em; +} + +.inputContainer { + flex: 1; +} + +.input { + border: 0; + margin: 0; + padding: 0; + color: black; + height: 100%; + font-size: 11px; + resize: none; + width: 100%; +} + +.container { + border-top: 1px solid lightgray; + display: flex; + flex: 1; + padding: 0.25em; + position: relative; +} diff --git a/js/src/dapps/console/Input/input.js b/js/src/dapps/console/Input/input.js new file mode 100644 index 000000000..3263aff38 --- /dev/null +++ b/js/src/dapps/console/Input/input.js @@ -0,0 +1,145 @@ +// Copyright 2015-2017 Parity Technologies (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import keycode from 'keycode'; +import { observer } from 'mobx-react'; +import React, { Component } from 'react'; +import ReactDOM from 'react-dom'; + +import Autocomplete from '../Autocomplete'; + +import AutocompleteStore from '../Autocomplete/autocomplete.store'; +import ConsoleStore from '../Console/console.store'; +import InputStore from './input.store'; +import SettingsStore from '../Settings/settings.store'; + +import styles from './input.css'; + +@observer +export default class Input extends Component { + autocompleteStore = AutocompleteStore.get(); + consoleStore = ConsoleStore.get(); + inputStore = InputStore.get(); + settingsStore = SettingsStore.get(); + + render () { + const { input } = this.inputStore; + + return ( +
+ + > +
+